mcp.go

  1package chat
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"strings"
  7
  8	"github.com/charmbracelet/crush/internal/message"
  9	"github.com/charmbracelet/crush/internal/stringext"
 10	"github.com/charmbracelet/crush/internal/ui/styles"
 11)
 12
 13// MCPToolMessageItem is a message item that represents a bash tool call.
 14type MCPToolMessageItem struct {
 15	*baseToolMessageItem
 16}
 17
 18var _ ToolMessageItem = (*MCPToolMessageItem)(nil)
 19
 20// NewMCPToolMessageItem creates a new [MCPToolMessageItem].
 21func NewMCPToolMessageItem(
 22	sty *styles.Styles,
 23	toolCall message.ToolCall,
 24	result *message.ToolResult,
 25	canceled bool,
 26) ToolMessageItem {
 27	return newBaseToolMessageItem(sty, toolCall, result, &MCPToolRenderContext{}, canceled)
 28}
 29
 30// MCPToolRenderContext renders bash tool messages.
 31type MCPToolRenderContext struct{}
 32
 33// RenderTool implements the [ToolRenderer] interface.
 34func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 35	toolNameParts := strings.SplitN(opts.ToolCall.Name, "_", 3)
 36	if len(toolNameParts) != 3 {
 37		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid tool name"}, width)
 38	}
 39	mcpName := prettyName(toolNameParts[1])
 40	toolName := prettyName(toolNameParts[2])
 41
 42	mcpName = sty.Tool.MCPName.Render(mcpName)
 43	toolName = sty.Tool.MCPToolName.Render(toolName)
 44
 45	name := fmt.Sprintf("%s %s %s", mcpName, sty.Tool.MCPArrow.String(), toolName)
 46
 47	if opts.IsPending() {
 48		return pendingTool(sty, name, opts.Anim)
 49	}
 50
 51	var params map[string]any
 52	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
 53		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
 54	}
 55
 56	var toolParams []string
 57	if len(params) > 0 {
 58		parsed, _ := json.Marshal(params)
 59		toolParams = append(toolParams, string(parsed))
 60	}
 61
 62	header := toolHeader(sty, opts.Status, name, width, opts.Compact, toolParams...)
 63	if opts.Compact {
 64		return header
 65	}
 66
 67	if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
 68		return joinToolParts(header, earlyState)
 69	}
 70
 71	if !opts.HasResult() || opts.Result.Content == "" {
 72		return header
 73	}
 74
 75	bodyWidth := width - toolBodyLeftPaddingTotal
 76	// see if the result is json
 77	var result json.RawMessage
 78	var body string
 79	if err := json.Unmarshal([]byte(opts.Result.Content), &result); err == nil {
 80		prettyResult, err := json.MarshalIndent(result, "", "  ")
 81		if err == nil {
 82			body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.json", string(prettyResult), 0, bodyWidth, opts.ExpandedContent))
 83		} else {
 84			body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
 85		}
 86	} else if looksLikeMarkdown(opts.Result.Content) {
 87		body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.md", opts.Result.Content, 0, bodyWidth, opts.ExpandedContent))
 88	} else {
 89		body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
 90	}
 91	return joinToolParts(header, body)
 92}
 93
 94func prettyName(name string) string {
 95	name = strings.ReplaceAll(name, "_", " ")
 96	name = strings.ReplaceAll(name, "-", " ")
 97	return stringext.Capitalize(name)
 98}
 99
100// looksLikeMarkdown checks if content appears to be markdown by looking for
101// common markdown patterns.
102func looksLikeMarkdown(content string) bool {
103	patterns := []string{
104		"# ",  // headers
105		"## ", // headers
106		"**",  // bold
107		"```", // code fence
108		"- ",  // unordered list
109		"1. ", // ordered list
110		"> ",  // blockquote
111		"---", // horizontal rule
112		"***", // horizontal rule
113	}
114	for _, p := range patterns {
115		if strings.Contains(content, p) {
116			return true
117		}
118	}
119	return false
120}