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, prompt, and resource counts for display.
 38func mcpCounts(t *styles.Styles, counts mcp.Counts) string {
 39	var 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	if counts.Resources > 0 {
 47		parts = append(parts, t.Subtle.Render(fmt.Sprintf("%d resources", counts.Resources)))
 48	}
 49	return strings.Join(parts, " ")
 50}
 51
 52// mcpList renders a list of MCP clients with their status and counts,
 53// truncating to maxItems if needed.
 54func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) string {
 55	if maxItems <= 0 {
 56		return ""
 57	}
 58	var renderedMcps []string
 59
 60	for _, m := range mcps {
 61		var icon string
 62		title := m.Name
 63		var description string
 64		var extraContent string
 65
 66		switch m.State {
 67		case mcp.StateStarting:
 68			icon = t.ItemBusyIcon.String()
 69			description = t.Subtle.Render("starting...")
 70		case mcp.StateConnected:
 71			icon = t.ItemOnlineIcon.String()
 72			extraContent = mcpCounts(t, m.Counts)
 73		case mcp.StateError:
 74			icon = t.ItemErrorIcon.String()
 75			description = t.Subtle.Render("error")
 76			if m.Error != nil {
 77				description = t.Subtle.Render(fmt.Sprintf("error: %s", m.Error.Error()))
 78			}
 79		case mcp.StateDisabled:
 80			icon = t.ItemOfflineIcon.Foreground(t.Muted.GetBackground()).String()
 81			description = t.Subtle.Render("disabled")
 82		default:
 83			icon = t.ItemOfflineIcon.String()
 84		}
 85
 86		renderedMcps = append(renderedMcps, common.Status(t, common.StatusOpts{
 87			Icon:         icon,
 88			Title:        title,
 89			Description:  description,
 90			ExtraContent: extraContent,
 91		}, width))
 92	}
 93
 94	if len(renderedMcps) > maxItems {
 95		visibleItems := renderedMcps[:maxItems-1]
 96		remaining := len(renderedMcps) - maxItems
 97		visibleItems = append(visibleItems, t.Subtle.Render(fmt.Sprintf("…and %d more", remaining)))
 98		return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
 99	}
100	return lipgloss.JoinVertical(lipgloss.Left, renderedMcps...)
101}