mcp.go

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