docker_mcp.go

  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, &params); 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}