mcp.go

  1package mcp
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/charmbracelet/lipgloss/v2"
  8
  9	"github.com/charmbracelet/crush/internal/config"
 10	"github.com/charmbracelet/crush/internal/llm/agent"
 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 := agent.GetMCPStates()
 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 agent.MCPStateDisabled:
 65				description = t.S().Subtle.Render("disabled")
 66			case agent.MCPStateStarting:
 67				icon = t.ItemBusyIcon
 68				description = t.S().Subtle.Render("starting...")
 69			case agent.MCPStateConnected:
 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				if count := state.Counts.Resources; count > 0 {
 78					extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d resources", count)))
 79				}
 80			case agent.MCPStateError:
 81				icon = t.ItemErrorIcon
 82				if state.Error != nil {
 83					description = t.S().Subtle.Render(fmt.Sprintf("error: %s", state.Error.Error()))
 84				} else {
 85					description = t.S().Subtle.Render("error")
 86				}
 87			}
 88		} else if l.MCP.Disabled {
 89			description = t.S().Subtle.Render("disabled")
 90		}
 91
 92		mcpList = append(mcpList,
 93			core.Status(
 94				core.StatusOpts{
 95					Icon:         icon.String(),
 96					Title:        l.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}