mcp_arguments.go

  1package commands
  2
  3import (
  4	"cmp"
  5	"context"
  6	"fmt"
  7	"log/slog"
  8	"strings"
  9
 10	"github.com/charmbracelet/bubbles/v2/help"
 11	"github.com/charmbracelet/bubbles/v2/key"
 12	"github.com/charmbracelet/bubbles/v2/textinput"
 13	tea "github.com/charmbracelet/bubbletea/v2"
 14	"github.com/charmbracelet/lipgloss/v2"
 15	"github.com/modelcontextprotocol/go-sdk/mcp"
 16
 17	"github.com/charmbracelet/crush/internal/llm/agent"
 18	"github.com/charmbracelet/crush/internal/tui/components/chat"
 19	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 20	"github.com/charmbracelet/crush/internal/tui/styles"
 21	"github.com/charmbracelet/crush/internal/tui/util"
 22)
 23
 24const mcpArgumentsDialogID dialogs.DialogID = "mcp_arguments"
 25
 26type MCPPromptArgumentsDialog interface {
 27	dialogs.DialogModel
 28}
 29
 30type mcpPromptArgumentsDialogCmp struct {
 31	wWidth, wHeight int
 32	width, height   int
 33	selected        int
 34	inputs          []textinput.Model
 35	keys            ArgumentsDialogKeyMap
 36	id              string
 37	prompt          *mcp.Prompt
 38	help            help.Model
 39}
 40
 41func NewMCPPromptArgumentsDialog(id, name string) MCPPromptArgumentsDialog {
 42	id = strings.TrimPrefix(id, MCPPromptPrefix)
 43	prompt, ok := agent.GetMCPPrompt(id)
 44	if !ok {
 45		return nil
 46	}
 47
 48	t := styles.CurrentTheme()
 49	inputs := make([]textinput.Model, len(prompt.Arguments))
 50
 51	for i, arg := range prompt.Arguments {
 52		ti := textinput.New()
 53		placeholder := fmt.Sprintf("Enter value for %s...", arg.Name)
 54		if arg.Description != "" {
 55			placeholder = arg.Description
 56		}
 57		ti.Placeholder = placeholder
 58		ti.SetWidth(40)
 59		ti.SetVirtualCursor(false)
 60		ti.Prompt = ""
 61		ti.SetStyles(t.S().TextInput)
 62
 63		if i == 0 {
 64			ti.Focus()
 65		} else {
 66			ti.Blur()
 67		}
 68
 69		inputs[i] = ti
 70	}
 71
 72	return &mcpPromptArgumentsDialogCmp{
 73		inputs: inputs,
 74		keys:   DefaultArgumentsDialogKeyMap(),
 75		id:     id,
 76		prompt: prompt,
 77		help:   help.New(),
 78	}
 79}
 80
 81func (c *mcpPromptArgumentsDialogCmp) Init() tea.Cmd {
 82	return nil
 83}
 84
 85func (c *mcpPromptArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 86	switch msg := msg.(type) {
 87	case tea.WindowSizeMsg:
 88		c.wWidth = msg.Width
 89		c.wHeight = msg.Height
 90		cmd := c.SetSize()
 91		return c, cmd
 92	case tea.KeyPressMsg:
 93		switch {
 94		case key.Matches(msg, c.keys.Cancel):
 95			return c, util.CmdHandler(dialogs.CloseDialogMsg{})
 96		case key.Matches(msg, c.keys.Confirm):
 97			if c.selected == len(c.inputs)-1 {
 98				args := make(map[string]string)
 99				for i, arg := range c.prompt.Arguments {
100					value := c.inputs[i].Value()
101					args[arg.Name] = value
102				}
103				return c, tea.Sequence(
104					util.CmdHandler(dialogs.CloseDialogMsg{}),
105					c.executeMCPPrompt(args),
106				)
107			}
108			c.inputs[c.selected].Blur()
109			c.selected++
110			c.inputs[c.selected].Focus()
111		case key.Matches(msg, c.keys.Next):
112			c.inputs[c.selected].Blur()
113			c.selected = (c.selected + 1) % len(c.inputs)
114			c.inputs[c.selected].Focus()
115		case key.Matches(msg, c.keys.Previous):
116			c.inputs[c.selected].Blur()
117			c.selected = (c.selected - 1 + len(c.inputs)) % len(c.inputs)
118			c.inputs[c.selected].Focus()
119		default:
120			var cmd tea.Cmd
121			c.inputs[c.selected], cmd = c.inputs[c.selected].Update(msg)
122			return c, cmd
123		}
124	}
125	return c, nil
126}
127
128func (c *mcpPromptArgumentsDialogCmp) executeMCPPrompt(args map[string]string) tea.Cmd {
129	return func() tea.Msg {
130		parts := strings.SplitN(c.id, ":", 2)
131		if len(parts) != 2 {
132			return util.ReportError(fmt.Errorf("invalid prompt ID: %s", c.id))
133		}
134		clientName := parts[0]
135
136		ctx := context.Background()
137		slog.Warn("AQUI", "name", c.prompt.Name, "id", c.id)
138		result, err := agent.GetMCPPromptContent(ctx, clientName, c.prompt.Name, args)
139		if err != nil {
140			return util.ReportError(err)
141		}
142
143		var content strings.Builder
144		for _, msg := range result.Messages {
145			if msg.Role == "user" {
146				if textContent, ok := msg.Content.(*mcp.TextContent); ok {
147					content.WriteString(textContent.Text)
148					content.WriteString("\n")
149				}
150			}
151		}
152
153		return chat.SendMsg{
154			Text: content.String(),
155		}
156	}
157}
158
159func (c *mcpPromptArgumentsDialogCmp) View() string {
160	t := styles.CurrentTheme()
161	baseStyle := t.S().Base
162
163	title := lipgloss.NewStyle().
164		Foreground(t.Primary).
165		Bold(true).
166		Padding(0, 1).
167		Render(cmp.Or(c.prompt.Title, c.prompt.Name))
168
169	promptName := t.S().Text.
170		Padding(0, 1).
171		Render(c.prompt.Description)
172
173	if c.prompt == nil {
174		return baseStyle.Padding(1, 1, 0, 1).
175			Border(lipgloss.RoundedBorder()).
176			BorderForeground(t.BorderFocus).
177			Width(c.width).
178			Render(lipgloss.JoinVertical(lipgloss.Left, title, promptName, "", "Prompt not found"))
179	}
180
181	inputFields := make([]string, len(c.inputs))
182	for i, input := range c.inputs {
183		labelStyle := baseStyle.Padding(1, 1, 0, 1)
184
185		if i == c.selected {
186			labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
187		} else {
188			labelStyle = labelStyle.Foreground(t.FgMuted)
189		}
190
191		argName := c.prompt.Arguments[i].Name
192		if c.prompt.Arguments[i].Required {
193			argName += " *"
194		}
195		label := labelStyle.Render(argName + ":")
196
197		field := t.S().Text.
198			Padding(0, 1).
199			Render(input.View())
200
201		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
202	}
203
204	elements := []string{title, promptName}
205	elements = append(elements, inputFields...)
206
207	c.help.ShowAll = false
208	helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
209	elements = append(elements, "", helpText)
210
211	content := lipgloss.JoinVertical(lipgloss.Left, elements...)
212
213	return baseStyle.Padding(1, 1, 0, 1).
214		Border(lipgloss.RoundedBorder()).
215		BorderForeground(t.BorderFocus).
216		Width(c.width).
217		Render(content)
218}
219
220func (c *mcpPromptArgumentsDialogCmp) Cursor() *tea.Cursor {
221	if len(c.inputs) == 0 {
222		return nil
223	}
224	cursor := c.inputs[c.selected].Cursor()
225	if cursor != nil {
226		cursor = c.moveCursor(cursor)
227	}
228	return cursor
229}
230
231const (
232	headerHeight      = 3
233	itemHeight        = 3
234	paddingHorizontal = 3
235)
236
237func (c *mcpPromptArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
238	row, col := c.Position()
239	offset := row + headerHeight + (1+c.selected)*itemHeight
240	cursor.Y += offset
241	cursor.X = cursor.X + col + paddingHorizontal
242	return cursor
243}
244
245func (c *mcpPromptArgumentsDialogCmp) SetSize() tea.Cmd {
246	c.width = min(90, c.wWidth)
247	c.height = min(15, c.wHeight)
248	for i := range c.inputs {
249		c.inputs[i].SetWidth(c.width - (paddingHorizontal * 2))
250	}
251	return nil
252}
253
254func (c *mcpPromptArgumentsDialogCmp) Position() (int, int) {
255	row := (c.wHeight / 2) - (c.height / 2)
256	col := (c.wWidth / 2) - (c.width / 2)
257	return row, col
258}
259
260func (c *mcpPromptArgumentsDialogCmp) ID() dialogs.DialogID {
261	return mcpArgumentsDialogID
262}