mcp.go

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