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(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 := config.Get().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		iconColor := t.FgMuted
 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				iconColor = t.FgMuted
 65				description = t.S().Subtle.Render("disabled")
 66			case agent.MCPStateStarting:
 67				iconColor = t.Yellow
 68				description = t.S().Subtle.Render("starting...")
 69			case agent.MCPStateConnected:
 70				iconColor = t.Success
 71				if state.ToolCount > 0 {
 72					extraContent = t.S().Subtle.Render(fmt.Sprintf("(%d tools)", state.ToolCount))
 73				}
 74			case agent.MCPStateError:
 75				iconColor = t.Red
 76				if state.Error != nil {
 77					description = t.S().Subtle.Render(fmt.Sprintf("error: %s", state.Error.Error()))
 78				} else {
 79					description = t.S().Subtle.Render("error")
 80				}
 81			}
 82		} else if l.MCP.Disabled {
 83			iconColor = t.FgMuted
 84			description = t.S().Subtle.Render("disabled")
 85		}
 86
 87		mcpList = append(mcpList,
 88			core.Status(
 89				core.StatusOpts{
 90					IconColor:    iconColor,
 91					Title:        l.Name,
 92					Description:  description,
 93					ExtraContent: extraContent,
 94				},
 95				opts.MaxWidth,
 96			),
 97		)
 98	}
 99
100	return mcpList
101}
102
103// RenderMCPBlock renders a complete MCP block with optional truncation indicator.
104func RenderMCPBlock(opts RenderOptions, showTruncationIndicator bool) string {
105	t := styles.CurrentTheme()
106	mcpList := RenderMCPList(opts)
107
108	// Add truncation indicator if needed
109	if showTruncationIndicator && opts.MaxItems > 0 {
110		mcps := config.Get().MCP.Sorted()
111		if len(mcps) > opts.MaxItems {
112			remaining := len(mcps) - opts.MaxItems
113			if remaining == 1 {
114				mcpList = append(mcpList, t.S().Base.Foreground(t.FgMuted).Render("…"))
115			} else {
116				mcpList = append(mcpList,
117					t.S().Base.Foreground(t.FgSubtle).Render(fmt.Sprintf("…and %d more", remaining)),
118				)
119			}
120		}
121	}
122
123	content := lipgloss.JoinVertical(lipgloss.Left, mcpList...)
124	if opts.MaxWidth > 0 {
125		return lipgloss.NewStyle().Width(opts.MaxWidth).Render(content)
126	}
127	return content
128}