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/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}