1package messages
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7
8 "charm.land/lipgloss/v2"
9 "charm.land/lipgloss/v2/table"
10 "github.com/charmbracelet/crush/internal/tui/styles"
11
12 "github.com/charmbracelet/crush/internal/stringext"
13)
14
15type dockerMCPRenderer struct {
16 baseRenderer
17}
18
19// Render displays file content with optional limit and offset parameters
20func (dr dockerMCPRenderer) Render(v *toolCallCmp) string {
21 var params map[string]any
22 if err := dr.unmarshalParams(v.call.Input, ¶ms); err != nil {
23 return dr.renderError(v, "Invalid view parameters")
24 }
25
26 tool := strings.ReplaceAll(v.call.Name, "mcp_crush_docker_", "")
27
28 main := v.call.Input
29 extraArgs := map[string]string{}
30 switch tool {
31 case "mcp-find":
32 if query, ok := params["query"]; ok {
33 if qStr, ok := query.(string); ok {
34 main = qStr
35 }
36 }
37 for k, v := range params {
38 if k == "query" {
39 continue
40 }
41
42 data, _ := json.Marshal(v)
43 extraArgs[k] = string(data)
44 }
45 case "mcp-add":
46 if name, ok := params["name"]; ok {
47 if nStr, ok := name.(string); ok {
48 main = nStr
49 }
50 }
51 for k, v := range params {
52 if k == "name" {
53 continue
54 }
55
56 data, _ := json.Marshal(v)
57 extraArgs[k] = string(data)
58 }
59 case "mcp-remove":
60 if name, ok := params["name"]; ok {
61 if nStr, ok := name.(string); ok {
62 main = nStr
63 }
64 }
65 for k, v := range params {
66 if k == "name" {
67 continue
68 }
69
70 data, _ := json.Marshal(v)
71 extraArgs[k] = string(data)
72 }
73 }
74
75 args := newParamBuilder().
76 addMain(main)
77
78 for k, v := range extraArgs {
79 args.addKeyValue(k, v)
80 }
81
82 width := v.textWidth()
83 if v.isNested {
84 width -= 4 // Adjust for nested tool call indentation
85 }
86 header := dr.makeHeader(v, tool, width, args.build()...)
87 if v.isNested {
88 return v.style().Render(header)
89 }
90 if res, done := earlyState(header, v); done {
91 return res
92 }
93
94 if tool == "mcp-find" {
95 return joinHeaderBody(header, dr.renderMCPServers(v))
96 }
97 return joinHeaderBody(header, renderPlainContent(v, v.result.Content))
98}
99
100type FindMCPResponse struct {
101 Servers []struct {
102 Name string `json:"name"`
103 Description string `json:"description"`
104 } `json:"servers"`
105}
106
107func (dr dockerMCPRenderer) renderMCPServers(v *toolCallCmp) string {
108 t := styles.CurrentTheme()
109 var result FindMCPResponse
110 if err := dr.unmarshalParams(v.result.Content, &result); err != nil {
111 return renderPlainContent(v, v.result.Content)
112 }
113
114 if len(result.Servers) == 0 {
115 return t.S().Muted.Render("No MCP servers found.")
116 }
117 width := min(120, v.textWidth()) - 2
118 rows := [][]string{}
119 moreServers := ""
120 for i, server := range result.Servers {
121 if i > 9 {
122 moreServers = t.S().Subtle.Render(fmt.Sprintf("... and %d mode", len(result.Servers)-10))
123 break
124 }
125 rows = append(rows, []string{t.S().Base.Render(server.Name), t.S().Muted.Render(server.Description)})
126 }
127 serverTable := table.New().
128 Wrap(false).
129 BorderTop(false).
130 BorderBottom(false).
131 BorderRight(false).
132 BorderLeft(false).
133 BorderColumn(false).
134 BorderRow(false).
135 StyleFunc(func(row, col int) lipgloss.Style {
136 if row == table.HeaderRow {
137 return lipgloss.NewStyle()
138 }
139 switch col {
140 case 0:
141 return lipgloss.NewStyle().PaddingRight(1)
142 }
143 return lipgloss.NewStyle()
144 }).Rows(rows...).Width(width)
145 if moreServers != "" {
146 return serverTable.Render() + "\n" + moreServers
147 }
148 return serverTable.Render()
149}
150
151func (dr dockerMCPRenderer) makeHeader(v *toolCallCmp, tool string, width int, params ...string) string {
152 t := styles.CurrentTheme()
153 mainTool := "Docker MCP"
154 action := tool
155 actionStyle := t.S().Base.Foreground(t.BlueDark)
156 switch tool {
157 case "mcp-exec":
158 action = "Exec"
159 case "mcp-config-set":
160 action = "Config Set"
161 case "mcp-find":
162 action = "Find"
163 case "mcp-add":
164 action = "Add"
165 actionStyle = t.S().Base.Foreground(t.GreenLight)
166 case "mcp-remove":
167 action = "Remove"
168 actionStyle = t.S().Base.Foreground(t.RedLighter)
169 default:
170 action = strings.ReplaceAll(tool, "-", " ")
171 action = strings.ReplaceAll(action, "_", " ")
172 action = stringext.Capitalize(action)
173 }
174 if v.isNested {
175 return dr.makeNestedHeader(v, tool, width, params...)
176 }
177 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
178 if v.result.ToolCallID != "" {
179 if v.result.IsError {
180 icon = t.S().Base.Foreground(t.RedDark).Render(styles.ToolError)
181 } else {
182 icon = t.S().Base.Foreground(t.Green).Render(styles.ToolSuccess)
183 }
184 } else if v.cancelled {
185 icon = t.S().Muted.Render(styles.ToolPending)
186 }
187 tool = t.S().Base.Foreground(t.Blue).Render(mainTool)
188 arrow := t.S().Base.Foreground(t.BlueDark).Render(styles.ArrowIcon)
189 prefix := fmt.Sprintf("%s %s %s %s ", icon, tool, arrow, actionStyle.Render(action))
190 return prefix + renderParamList(false, width-lipgloss.Width(prefix), params...)
191}