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