package mcp import ( "context" "encoding/json" "fmt" "iter" "log/slog" "strings" "github.com/charmbracelet/crush/internal/csync" "github.com/modelcontextprotocol/go-sdk/mcp" ) type Tool = mcp.Tool // ToolResult represents the result of running an MCP tool. type ToolResult struct { Type string Content string Data []byte MediaType string } var allTools = csync.NewMap[string, []*Tool]() // Tools returns all available MCP tools. func Tools() iter.Seq2[string, []*Tool] { return allTools.Seq2() } // RunTool runs an MCP tool with the given input parameters. func RunTool(ctx context.Context, name, toolName string, input string) (ToolResult, error) { var args map[string]any if err := json.Unmarshal([]byte(input), &args); err != nil { return ToolResult{}, fmt.Errorf("error parsing parameters: %s", err) } c, err := getOrRenewClient(ctx, name) if err != nil { return ToolResult{}, err } result, err := c.CallTool(ctx, &mcp.CallToolParams{ Name: toolName, Arguments: args, }) if err != nil { return ToolResult{}, err } if len(result.Content) == 0 { return ToolResult{Type: "text", Content: ""}, nil } var textParts []string var imageData []byte var imageMimeType string var audioData []byte var audioMimeType string for _, v := range result.Content { switch content := v.(type) { case *mcp.TextContent: textParts = append(textParts, content.Text) case *mcp.ImageContent: if imageData == nil { imageData = content.Data imageMimeType = content.MIMEType } case *mcp.AudioContent: if audioData == nil { audioData = content.Data audioMimeType = content.MIMEType } default: textParts = append(textParts, fmt.Sprintf("%v", v)) } } textContent := strings.Join(textParts, "\n") // MCP SDK returns Data as already base64-encoded, so we use it directly. if imageData != nil { return ToolResult{ Type: "image", Content: textContent, Data: imageData, MediaType: imageMimeType, }, nil } if audioData != nil { return ToolResult{ Type: "media", Content: textContent, Data: audioData, MediaType: audioMimeType, }, nil } return ToolResult{ Type: "text", Content: textContent, }, nil } // RefreshTools gets the updated list of tools from the MCP and updates the // global state. func RefreshTools(ctx context.Context, name string) { session, ok := sessions.Get(name) if !ok { slog.Warn("refresh tools: no session", "name", name) return } tools, err := getTools(ctx, session) if err != nil { updateState(name, StateError, err, nil, Counts{}) return } updateTools(name, tools) prev, _ := states.Get(name) prev.Counts.Tools = len(tools) updateState(name, StateConnected, nil, session, prev.Counts) } func getTools(ctx context.Context, session *mcp.ClientSession) ([]*Tool, error) { // Always call ListTools to get the actual available tools. // The InitializeResult Capabilities.Tools field may be an empty object {}, // which is valid per MCP spec, but we still need to call ListTools to discover tools. result, err := session.ListTools(ctx, &mcp.ListToolsParams{}) if err != nil { return nil, err } return result.Tools, nil } func updateTools(name string, tools []*Tool) { if len(tools) == 0 { allTools.Del(name) return } allTools.Set(name, tools) }