@@ -0,0 +1,290 @@
8+ "charm.land/lipgloss/v2"
9+ "charm.land/lipgloss/v2/table"
10+ "github.com/charmbracelet/crush/internal/config"
11+ "github.com/charmbracelet/crush/internal/message"
12+ "github.com/charmbracelet/crush/internal/stringext"
13+ "github.com/charmbracelet/crush/internal/ui/styles"
16+// DockerMCPToolMessageItem is a message item that represents a Docker MCP tool call.
17+type DockerMCPToolMessageItem struct {
21+var _ ToolMessageItem = (*DockerMCPToolMessageItem)(nil)
23+// NewDockerMCPToolMessageItem creates a new [DockerMCPToolMessageItem].
24+func NewDockerMCPToolMessageItem(
26+ toolCall message.ToolCall,
27+ result *message.ToolResult,
30+ return newBaseToolMessageItem(sty, toolCall, result, &DockerMCPToolRenderContext{}, canceled)
33+// DockerMCPToolRenderContext renders Docker MCP tool messages.
34+type DockerMCPToolRenderContext struct{}
36+// RenderTool implements the [ToolRenderer] interface.
37+func (d *DockerMCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
38+ cappedWidth := cappedMessageWidth(width)
40+ var params map[string]any
41+ if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil {
42+ params = make(map[string]any)
45+ tool := strings.TrimPrefix(opts.ToolCall.Name, "mcp_"+config.DockerMCPName+"_")
47+ mainParam := opts.ToolCall.Input
48+ extraArgs := map[string]string{}
51+ if query, ok := params["query"]; ok {
52+ if qStr, ok := query.(string); ok {
56+ for k, v := range params {
60+ data, _ := json.Marshal(v)
61+ extraArgs[k] = string(data)
64+ if name, ok := params["name"]; ok {
65+ if nStr, ok := name.(string); ok {
69+ for k, v := range params {
73+ data, _ := json.Marshal(v)
74+ extraArgs[k] = string(data)
77+ if name, ok := params["name"]; ok {
78+ if nStr, ok := name.(string); ok {
82+ for k, v := range params {
86+ data, _ := json.Marshal(v)
87+ extraArgs[k] = string(data)
90+ if name, ok := params["name"]; ok {
91+ if nStr, ok := name.(string); ok {
95+ case "mcp-config-set":
96+ if server, ok := params["server"]; ok {
97+ if sStr, ok := server.(string); ok {
103+ var toolParams []string
104+ toolParams = append(toolParams, mainParam)
105+ for k, v := range extraArgs {
106+ toolParams = append(toolParams, k, v)
109+ if opts.IsPending() {
110+ return pendingTool(sty, d.formatToolName(sty, tool), opts.Anim, false)
113+ header := d.makeHeader(sty, tool, cappedWidth, opts, toolParams...)
118+ if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
119+ return joinToolParts(header, earlyState)
122+ if tool == "mcp-find" {
123+ return joinToolParts(header, d.renderMCPServers(sty, opts, cappedWidth))
126+ if !opts.HasResult() {
130+ bodyWidth := cappedWidth - toolBodyLeftPaddingTotal
133+ // Handle text content.
134+ if opts.Result.Content != "" {
136+ var result json.RawMessage
137+ if err := json.Unmarshal([]byte(opts.Result.Content), &result); err == nil {
138+ prettyResult, err := json.MarshalIndent(result, "", " ")
140+ body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.json", string(prettyResult), 0, bodyWidth, opts.ExpandedContent))
142+ body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
144+ } else if looksLikeMarkdown(opts.Result.Content) {
145+ body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.md", opts.Result.Content, 0, bodyWidth, opts.ExpandedContent))
147+ body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
149+ parts = append(parts, body)
152+ // Handle image content.
153+ if opts.Result.Data != "" && strings.HasPrefix(opts.Result.MIMEType, "image/") {
154+ parts = append(parts, "", toolOutputImageContent(sty, opts.Result.Data, opts.Result.MIMEType))
157+ if len(parts) == 0 {
161+ return joinToolParts(header, strings.Join(parts, "\n"))
164+// FindMCPResponse represents the response from mcp-find.
165+type FindMCPResponse struct {
167+ Name string `json:"name"`
168+ Description string `json:"description"`
172+func (d *DockerMCPToolRenderContext) renderMCPServers(sty *styles.Styles, opts *ToolRenderOpts, width int) string {
173+ if !opts.HasResult() || opts.Result.Content == "" {
177+ var result FindMCPResponse
178+ if err := json.Unmarshal([]byte(opts.Result.Content), &result); err != nil {
179+ return toolOutputPlainContent(sty, opts.Result.Content, width-toolBodyLeftPaddingTotal, opts.ExpandedContent)
182+ if len(result.Servers) == 0 {
183+ return sty.Subtle.Render("No MCP servers found.")
186+ bodyWidth := min(120, width) - toolBodyLeftPaddingTotal
187+ rows := [][]string{}
189+ for i, server := range result.Servers {
191+ moreServers = sty.Subtle.Render(fmt.Sprintf("... and %d more", len(result.Servers)-10))
194+ rows = append(rows, []string{sty.Base.Render(server.Name), sty.Subtle.Render(server.Description)})
196+ serverTable := table.New().
199+ BorderBottom(false).
202+ BorderColumn(false).
204+ StyleFunc(func(row, col int) lipgloss.Style {
205+ if row == table.HeaderRow {
206+ return lipgloss.NewStyle()
210+ return lipgloss.NewStyle().PaddingRight(1)
212+ return lipgloss.NewStyle()
213+ }).Rows(rows...).Width(bodyWidth)
214+ if moreServers != "" {
215+ return sty.Tool.Body.Render(serverTable.Render() + "\n" + moreServers)
217+ return sty.Tool.Body.Render(serverTable.Render())
220+func (d *DockerMCPToolRenderContext) makeHeader(sty *styles.Styles, tool string, width int, opts *ToolRenderOpts, params ...string) string {
222+ return d.makeCompactHeader(sty, tool, width, params...)
225+ icon := toolIcon(sty, opts.Status)
226+ if opts.IsPending() {
227+ icon = sty.Tool.IconPending.Render()
229+ prefix := fmt.Sprintf("%s %s ", icon, d.formatToolName(sty, tool))
230+ return prefix + toolParamList(sty, params, width-lipgloss.Width(prefix))
233+func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool string) string {
234+ mainTool := "Docker MCP"
236+ actionStyle := sty.Tool.MCPToolName
240+ case "mcp-config-set":
241+ action = "Config Set"
246+ actionStyle = sty.Tool.DockerMCPActionAdd
249+ actionStyle = sty.Tool.DockerMCPActionDel
251+ action = "Code Mode"
253+ action = strings.ReplaceAll(tool, "-", " ")
254+ action = strings.ReplaceAll(action, "_", " ")
255+ action = stringext.Capitalize(action)
258+ toolNameStyled := sty.Tool.MCPName.Render(mainTool)
259+ arrow := sty.Tool.MCPArrow.String()
260+ return fmt.Sprintf("%s %s %s", toolNameStyled, arrow, actionStyle.Render(action))
263+func (d *DockerMCPToolRenderContext) makeCompactHeader(sty *styles.Styles, tool string, width int, params ...string) string {
268+ case "mcp-config-set":
269+ action = "config-set"
277+ action = "code-mode"
279+ action = strings.ReplaceAll(tool, "-", " ")
280+ action = strings.ReplaceAll(action, "_", " ")
283+ name := fmt.Sprintf("Docker MCP: %s", action)
284+ return toolHeader(sty, ToolStatusSuccess, name, width, true, params...)
287+// IsDockerMCPTool returns true if the tool name is a Docker MCP tool.
288+func IsDockerMCPTool(name string) bool {
289+ return strings.HasPrefix(name, "mcp_"+config.DockerMCPName+"_")