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					label := "tools"
 73					if count == 1 {
 74						label = "tool"
 75					}
 76					extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d %s", count, label)))
 77				}
 78				if count := state.Counts.Prompts; count > 0 {
 79					label := "prompts"
 80					if count == 1 {
 81						label = "prompt"
 82					}
 83					extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d %s", count, label)))
 84				}
 85			case mcp.StateError:
 86				icon = t.ItemErrorIcon
 87				if state.Error != nil {
 88					description = t.S().Subtle.Render(fmt.Sprintf("error: %s", state.Error.Error()))
 89				} else {
 90					description = t.S().Subtle.Render("error")
 91				}
 92			}
 93		} else if l.MCP.Disabled {
 94			description = t.S().Subtle.Render("disabled")
 95		}
 96
 97		mcpList = append(mcpList,
 98			core.Status(
 99				core.StatusOpts{
100					Icon:         icon.String(),
101					Title:        l.Name,
102					Description:  description,
103					ExtraContent: strings.Join(extraContent, " "),
104				},
105				opts.MaxWidth,
106			),
107		)
108	}
109
110	return mcpList
111}
112
113// RenderMCPBlock renders a complete MCP block with optional truncation indicator.
114func RenderMCPBlock(opts RenderOptions, showTruncationIndicator bool) string {
115	t := styles.CurrentTheme()
116	mcpList := RenderMCPList(opts)
117
118	// Add truncation indicator if needed
119	if showTruncationIndicator && opts.MaxItems > 0 {
120		mcps := config.Get().MCP.Sorted()
121		if len(mcps) > opts.MaxItems {
122			remaining := len(mcps) - opts.MaxItems
123			if remaining == 1 {
124				mcpList = append(mcpList, t.S().Base.Foreground(t.FgMuted).Render("…"))
125			} else {
126				mcpList = append(mcpList,
127					t.S().Base.Foreground(t.FgSubtle).Render(fmt.Sprintf("…and %d more", remaining)),
128				)
129			}
130		}
131	}
132
133	content := lipgloss.JoinVertical(lipgloss.Left, mcpList...)
134	if opts.MaxWidth > 0 {
135		return lipgloss.NewStyle().Width(opts.MaxWidth).Render(content)
136	}
137	return content
138}