1package model
2
3import (
4 "fmt"
5 "strings"
6
7 "charm.land/lipgloss/v2"
8 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/ui/common"
11 "github.com/charmbracelet/crush/internal/ui/styles"
12)
13
14// mcpInfo renders the MCP status section showing active MCP clients and their
15// tool/prompt counts.
16func (m *UI) mcpInfo(width, maxItems int, isSection bool) string {
17 var mcps []mcp.ClientInfo
18 t := m.com.Styles
19
20 for _, mcp := range m.com.Config().MCP.Sorted() {
21 if state, ok := m.mcpStates[mcp.Name]; ok {
22 mcps = append(mcps, state)
23 }
24 }
25
26 title := t.ResourceGroupTitle.Render("MCPs")
27 if isSection {
28 title = common.Section(t, title, width)
29 }
30 list := t.ResourceAdditionalText.Render("None")
31 if len(mcps) > 0 {
32 list = mcpList(t, mcps, width, maxItems)
33 }
34
35 return lipgloss.NewStyle().Width(width).Render(fmt.Sprintf("%s\n\n%s", title, list))
36}
37
38// mcpCounts formats tool, prompt, and resource counts for display.
39func mcpCounts(t *styles.Styles, counts mcp.Counts) string {
40 var parts []string
41 if counts.Tools > 0 {
42 parts = append(parts, t.Subtle.Render(fmt.Sprintf("%d tools", counts.Tools)))
43 }
44 if counts.Prompts > 0 {
45 parts = append(parts, t.Subtle.Render(fmt.Sprintf("%d prompts", counts.Prompts)))
46 }
47 if counts.Resources > 0 {
48 parts = append(parts, t.Subtle.Render(fmt.Sprintf("%d resources", counts.Resources)))
49 }
50 return strings.Join(parts, " ")
51}
52
53// mcpList renders a list of MCP clients with their status and counts,
54// truncating to maxItems if needed.
55func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) string {
56 if maxItems <= 0 {
57 return ""
58 }
59 var renderedMcps []string
60
61 for _, m := range mcps {
62 var icon string
63 title := m.Name
64 // Show "Docker MCP" instead of the config name for Docker MCP.
65 if m.Name == config.DockerMCPName {
66 title = "Docker MCP"
67 }
68 title = t.ResourceName.Render(title)
69 var description string
70 var extraContent string
71
72 switch m.State {
73 case mcp.StateStarting:
74 icon = t.ResourceBusyIcon.String()
75 description = t.ResourceStatus.Render("starting...")
76 case mcp.StateConnected:
77 icon = t.ResourceOnlineIcon.String()
78 extraContent = mcpCounts(t, m.Counts)
79 case mcp.StateError:
80 icon = t.ResourceErrorIcon.String()
81 description = t.ResourceStatus.Render("error")
82 if m.Error != nil {
83 description = t.ResourceStatus.Render(fmt.Sprintf("error: %s", m.Error.Error()))
84 }
85 case mcp.StateDisabled:
86 icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String()
87 description = t.ResourceStatus.Render("disabled")
88 default:
89 icon = t.ResourceOfflineIcon.String()
90 }
91
92 renderedMcps = append(renderedMcps, common.Status(t, common.StatusOpts{
93 Icon: icon,
94 Title: title,
95 Description: description,
96 ExtraContent: extraContent,
97 }, width))
98 }
99
100 if len(renderedMcps) > maxItems {
101 visibleItems := renderedMcps[:maxItems-1]
102 remaining := len(renderedMcps) - maxItems
103 visibleItems = append(visibleItems, t.ResourceAdditionalText.Render(fmt.Sprintf("β¦and %d more", remaining)))
104 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
105 }
106 return lipgloss.JoinVertical(lipgloss.Left, renderedMcps...)
107}