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	cappedWidth := cappedMessageWidth(width)
 36	toolNameParts := strings.SplitN(opts.ToolCall.Name, "_", 3)
 37	if len(toolNameParts) != 3 {
 38		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid tool name"}, cappedWidth)
 39	}
 40	mcpName := prettyName(toolNameParts[1])
 41	toolName := prettyName(toolNameParts[2])
 42
 43	mcpName = sty.Tool.MCPName.Render(mcpName)
 44	toolName = sty.Tool.MCPToolName.Render(toolName)
 45
 46	name := fmt.Sprintf("%s %s %s", mcpName, sty.Tool.MCPArrow.String(), toolName)
 47
 48	if opts.IsPending() {
 49		return pendingTool(sty, name, opts.Anim)
 50	}
 51
 52	var params map[string]any
 53	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
 54		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
 55	}
 56
 57	var toolParams []string
 58	if len(params) > 0 {
 59		parsed, _ := json.Marshal(params)
 60		toolParams = append(toolParams, string(parsed))
 61	}
 62
 63	header := toolHeader(sty, opts.Status, name, cappedWidth, opts.Compact, toolParams...)
 64	if opts.Compact {
 65		return header
 66	}
 67
 68	if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
 69		return joinToolParts(header, earlyState)
 70	}
 71
 72	if !opts.HasResult() || opts.Result.Content == "" {
 73		return header
 74	}
 75
 76	bodyWidth := cappedWidth - toolBodyLeftPaddingTotal
 77	// see if the result is json
 78	var result json.RawMessage
 79	var body string
 80	if err := json.Unmarshal([]byte(opts.Result.Content), &result); err == nil {
 81		prettyResult, err := json.MarshalIndent(result, "", "  ")
 82		if err == nil {
 83			body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.json", string(prettyResult), 0, bodyWidth, opts.ExpandedContent))
 84		} else {
 85			body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
 86		}
 87	} else if looksLikeMarkdown(opts.Result.Content) {
 88		body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.md", opts.Result.Content, 0, bodyWidth, opts.ExpandedContent))
 89	} else {
 90		body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
 91	}
 92	return joinToolParts(header, body)
 93}
 94
 95func prettyName(name string) string {
 96	name = strings.ReplaceAll(name, "_", " ")
 97	name = strings.ReplaceAll(name, "-", " ")
 98	return stringext.Capitalize(name)
 99}
100
101// looksLikeMarkdown checks if content appears to be markdown by looking for
102// common markdown patterns.
103func looksLikeMarkdown(content string) bool {
104	patterns := []string{
105		"# ",  // headers
106		"## ", // headers
107		"**",  // bold
108		"```", // code fence
109		"- ",  // unordered list
110		"1. ", // ordered list
111		"> ",  // blockquote
112		"---", // horizontal rule
113		"***", // horizontal rule
114	}
115	for _, p := range patterns {
116		if strings.Contains(content, p) {
117			return true
118		}
119	}
120	return false
121}