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}