mcp.go

  1package mcp
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"charm.land/lipgloss/v2"
  8
  9	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 10	"github.com/charmbracelet/crush/internal/config"
 11	"github.com/charmbracelet/crush/internal/tui/components/core"
 12	"github.com/charmbracelet/crush/internal/tui/styles"
 13)
 14
 15// RenderOptions contains options for rendering MCP lists.
 16type RenderOptions struct {
 17	MaxWidth    int
 18	MaxItems    int
 19	ShowSection bool
 20	SectionName string
 21}
 22
 23// RenderMCPList renders a list of MCP status items with the given options.
 24func RenderMCPList(opts RenderOptions) []string {
 25	t := styles.CurrentTheme()
 26	mcpList := []string{}
 27
 28	if opts.ShowSection {
 29		sectionName := opts.SectionName
 30		if sectionName == "" {
 31			sectionName = "MCPs"
 32		}
 33		section := t.S().Subtle.Render(sectionName)
 34		mcpList = append(mcpList, section, "")
 35	}
 36
 37	mcps := config.Get().MCP.Sorted()
 38	if len(mcps) == 0 {
 39		mcpList = append(mcpList, t.S().Base.Foreground(t.Border).Render("None"))
 40		return mcpList
 41	}
 42
 43	// Get MCP states
 44	mcpStates := mcp.GetStates()
 45
 46	// Determine how many items to show
 47	maxItems := len(mcps)
 48	if opts.MaxItems > 0 {
 49		maxItems = min(opts.MaxItems, len(mcps))
 50	}
 51
 52	for i, l := range mcps {
 53		if i >= maxItems {
 54			break
 55		}
 56
 57		// Determine icon and color based on state
 58		icon := t.ItemOfflineIcon
 59		description := ""
 60		extraContent := []string{}
 61
 62		if state, exists := mcpStates[l.Name]; exists {
 63			switch state.State {
 64			case mcp.StateDisabled:
 65				description = t.S().Subtle.Render("disabled")
 66			case mcp.StateStarting:
 67				icon = t.ItemBusyIcon
 68				description = t.S().Subtle.Render("starting...")
 69			case mcp.StateConnected:
 70				icon = t.ItemOnlineIcon
 71				if count := state.Counts.Tools; count > 0 {
 72					extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d tools", count)))
 73				}
 74				if count := state.Counts.Prompts; count > 0 {
 75					extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d prompts", count)))
 76				}
 77			case mcp.StateError:
 78				icon = t.ItemErrorIcon
 79				if state.Error != nil {
 80					description = t.S().Subtle.Render(fmt.Sprintf("error: %s", state.Error.Error()))
 81				} else {
 82					description = t.S().Subtle.Render("error")
 83				}
 84			}
 85		} else if l.MCP.Disabled {
 86			description = t.S().Subtle.Render("disabled")
 87		}
 88		name := l.Name
 89		if l.Name == "crush_docker" {
 90			name = "Docker MCP"
 91		}
 92		mcpList = append(mcpList,
 93			core.Status(
 94				core.StatusOpts{
 95					Icon:         icon.String(),
 96					Title:        name,
 97					Description:  description,
 98					ExtraContent: strings.Join(extraContent, " "),
 99				},
100				opts.MaxWidth,
101			),
102		)
103	}
104
105	return mcpList
106}
107
108// RenderMCPBlock renders a complete MCP block with optional truncation indicator.
109func RenderMCPBlock(opts RenderOptions, showTruncationIndicator bool) string {
110	t := styles.CurrentTheme()
111	mcpList := RenderMCPList(opts)
112
113	// Add truncation indicator if needed
114	if showTruncationIndicator && opts.MaxItems > 0 {
115		mcps := config.Get().MCP.Sorted()
116		if len(mcps) > opts.MaxItems {
117			remaining := len(mcps) - opts.MaxItems
118			if remaining == 1 {
119				mcpList = append(mcpList, t.S().Base.Foreground(t.FgMuted).Render("…"))
120			} else {
121				mcpList = append(mcpList,
122					t.S().Base.Foreground(t.FgSubtle).Render(fmt.Sprintf("…and %d more", remaining)),
123				)
124			}
125		}
126	}
127
128	content := lipgloss.JoinVertical(lipgloss.Left, mcpList...)
129	if opts.MaxWidth > 0 {
130		return lipgloss.NewStyle().Width(opts.MaxWidth).Render(content)
131	}
132	return content
133}