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}