Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/fsext/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func ListDirectory(initialPath string, ignorePatterns []string, depth, limit int
found := csync.NewSlice[string]()
dl := NewDirectoryLister(initialPath)

slog.Warn("listing directory", "path", initialPath, "depth", depth, "limit", limit, "ignorePatterns", ignorePatterns)
slog.Debug("listing directory", "path", initialPath, "depth", depth, "limit", limit, "ignorePatterns", ignorePatterns)

conf := fastwalk.Config{
Follow: true,
Expand Down
21 changes: 13 additions & 8 deletions internal/llm/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ func NewAgent(
}

baseToolsFn := func() map[string]tools.BaseTool {
slog.Info("Initializing agent base tools", "agent", agentCfg.ID)
slog.Debug("Initializing agent base tools", "agent", agentCfg.ID)
defer func() {
slog.Info("Initialized agent base tools", "agent", agentCfg.ID)
slog.Debug("Initialized agent base tools", "agent", agentCfg.ID)
}()

// Base tools available to all agents
Expand All @@ -202,9 +202,9 @@ func NewAgent(
return result
}
mcpToolsFn := func() map[string]tools.BaseTool {
slog.Info("Initializing agent mcp tools", "agent", agentCfg.ID)
slog.Debug("Initializing agent mcp tools", "agent", agentCfg.ID)
defer func() {
slog.Info("Initialized agent mcp tools", "agent", agentCfg.ID)
slog.Debug("Initialized agent mcp tools", "agent", agentCfg.ID)
}()

mcpToolsOnce.Do(func() {
Expand Down Expand Up @@ -346,9 +346,6 @@ func (a *agent) err(err error) AgentEvent {
}

func (a *agent) Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error) {
if !a.Model().SupportsImages && attachments != nil {
attachments = nil
}
events := make(chan AgentEvent, 1)
if a.IsSessionBusy(sessionID) {
existing, ok := a.promptQueue.Get(sessionID)
Expand All @@ -371,7 +368,15 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
})
var attachmentParts []message.ContentPart
for _, attachment := range attachments {
attachmentParts = append(attachmentParts, message.BinaryContent{Path: attachment.FilePath, MIMEType: attachment.MimeType, Data: attachment.Content})
if !a.Model().SupportsImages && strings.HasPrefix(attachment.MimeType, "image/") {
slog.Warn("Model does not support images, skipping attachment", "mimeType", attachment.MimeType, "fileName", attachment.FileName)
continue
}
attachmentParts = append(attachmentParts, message.BinaryContent{
Path: attachment.FilePath,
MIMEType: attachment.MimeType,
Data: attachment.Content,
})
}
result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
if result.Error != nil {
Expand Down
90 changes: 78 additions & 12 deletions internal/llm/agent/mcp-tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ type MCPEvent struct {

// MCPCounts number of available tools, prompts, etc.
type MCPCounts struct {
Tools int
Prompts int
Tools int
Prompts int
Resources int
}

// MCPClientInfo holds information about an MCP client's state
Expand All @@ -84,14 +85,16 @@ type MCPClientInfo struct {
}

var (
mcpToolsOnce sync.Once
mcpTools = csync.NewMap[string, tools.BaseTool]()
mcpClient2Tools = csync.NewMap[string, []tools.BaseTool]()
mcpClients = csync.NewMap[string, *mcp.ClientSession]()
mcpStates = csync.NewMap[string, MCPClientInfo]()
mcpBroker = pubsub.NewBroker[MCPEvent]()
mcpPrompts = csync.NewMap[string, *mcp.Prompt]()
mcpClient2Prompts = csync.NewMap[string, []*mcp.Prompt]()
mcpToolsOnce sync.Once
mcpTools = csync.NewMap[string, tools.BaseTool]()
mcpClient2Tools = csync.NewMap[string, []tools.BaseTool]()
mcpClients = csync.NewMap[string, *mcp.ClientSession]()
mcpStates = csync.NewMap[string, MCPClientInfo]()
mcpBroker = pubsub.NewBroker[MCPEvent]()
mcpPrompts = csync.NewMap[string, *mcp.Prompt]()
mcpClient2Prompts = csync.NewMap[string, []*mcp.Prompt]()
mcpResources = csync.NewMap[string, *mcp.Resource]()
mcpClient2Resources = csync.NewMap[string, []*mcp.Resource]()
)

type McpTool struct {
Expand Down Expand Up @@ -327,12 +330,22 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
return
}

resources, err := getResources(ctx, c)
if err != nil {
slog.Error("error listing resources", "error", err)
updateMCPState(name, MCPStateError, err, nil, MCPCounts{})
c.Close()
return
}

updateMcpTools(name, tools)
updateMcpPrompts(name, prompts)
updateMcpResources(name, resources)
mcpClients.Set(name, c)
counts := MCPCounts{
Tools: len(tools),
Prompts: len(prompts),
Tools: len(tools),
Prompts: len(prompts),
Resources: len(resources),
}
updateMCPState(name, MCPStateConnected, nil, c, counts)
}(name, m)
Expand Down Expand Up @@ -496,6 +509,32 @@ func updateMcpPrompts(mcpName string, prompts []*mcp.Prompt) {
}
}

func getResources(ctx context.Context, c *mcp.ClientSession) ([]*mcp.Resource, error) {
if c.InitializeResult().Capabilities.Resources == nil {
return nil, nil
}
result, err := c.ListResources(ctx, &mcp.ListResourcesParams{})
if err != nil {
return nil, err
}
return result.Resources, nil
}

// updateMcpResources updates the global mcpResources and mcpClient2Resources maps.
func updateMcpResources(mcpName string, resources []*mcp.Resource) {
if len(resources) == 0 {
mcpClient2Resources.Del(mcpName)
} else {
mcpClient2Resources.Set(mcpName, resources)
}
for clientName, resources := range mcpClient2Resources.Seq2() {
for _, p := range resources {
key := clientName + ":" + p.Name
mcpResources.Set(key, p)
}
}
}

// GetMCPPrompts returns all available MCP prompts.
func GetMCPPrompts() map[string]*mcp.Prompt {
return maps.Collect(mcpPrompts.Seq2())
Expand Down Expand Up @@ -523,3 +562,30 @@ func GetMCPPromptContent(ctx context.Context, clientName, promptName string, arg
Arguments: args,
})
}

// GetMCPResources returns all available MCP resources.
func GetMCPResources() map[string]*mcp.Resource {
return maps.Collect(mcpResources.Seq2())
}

// GetMCPResource returns a specific MCP resource by name.
func GetMCPResource(name string) (*mcp.Resource, bool) {
return mcpResources.Get(name)
}

// GetMCPResourcesByClient returns all resources for a specific MCP client.
func GetMCPResourcesByClient(clientName string) ([]*mcp.Resource, bool) {
return mcpClient2Resources.Get(clientName)
}

// GetMCPResourceContent retrieves the content of an MCP resource.
func GetMCPResourceContent(ctx context.Context, clientName, uri string) (*mcp.ReadResourceResult, error) {
c, err := getOrRenewClient(ctx, clientName)
if err != nil {
return nil, err
}

return c.ReadResource(ctx, &mcp.ReadResourceParams{
URI: uri,
})
}
13 changes: 10 additions & 3 deletions internal/llm/provider/anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,16 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
var contentBlocks []anthropic.ContentBlockParamUnion
contentBlocks = append(contentBlocks, content)
for _, binaryContent := range msg.BinaryContent() {
base64Image := binaryContent.String(catwalk.InferenceProviderAnthropic)
imageBlock := anthropic.NewImageBlockBase64(binaryContent.MIMEType, base64Image)
contentBlocks = append(contentBlocks, imageBlock)
if strings.HasPrefix(binaryContent.MIMEType, "image/") {
base64Image := binaryContent.String(catwalk.InferenceProviderAnthropic)
imageBlock := anthropic.NewImageBlockBase64(binaryContent.MIMEType, base64Image)
contentBlocks = append(contentBlocks, imageBlock)
continue
}
blk := anthropic.NewDocumentBlock(anthropic.PlainTextSourceParam{
Data: string(binaryContent.Data),
})
contentBlocks = append(contentBlocks, blk)
}
anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(contentBlocks...))

Expand Down
21 changes: 15 additions & 6 deletions internal/tui/components/chat/editor/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/charmbracelet/crush/internal/tui/components/dialogs"
"github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
"github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
"github.com/charmbracelet/crush/internal/tui/components/dialogs/mcp"
"github.com/charmbracelet/crush/internal/tui/components/dialogs/quit"
"github.com/charmbracelet/crush/internal/tui/styles"
"github.com/charmbracelet/crush/internal/tui/util"
Expand Down Expand Up @@ -179,10 +180,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.repositionCompletions
case filepicker.FilePickedMsg:
if len(m.attachments) >= maxAttachments {
return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments))
// TODO: check if this still needed
return m, util.ReportError(fmt.Errorf("cannot add more than %d attachments", maxAttachments))
}
m.attachments = append(m.attachments, msg.Attachment)
return m, nil
case mcp.ResourcePickedMsg:
m.attachments = append(m.attachments, msg.Attachment)
case completions.CompletionsOpenedMsg:
m.isCompletionsOpen = true
case completions.CompletionsClosedMsg:
Expand Down Expand Up @@ -458,16 +462,21 @@ func (m *editorCmp) attachmentsContent() string {
Background(t.FgMuted).
Foreground(t.FgBase)
for i, attachment := range m.attachments {
var filename string
icon := styles.DocumentIcon
if strings.HasPrefix(attachment.MimeType, "image/") {
icon = styles.ImageIcon
}

var item string
if len(attachment.FileName) > 10 {
filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, attachment.FileName[0:7])
item = fmt.Sprintf(" %s %s...", icon, attachment.FileName[0:7])
} else {
filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, attachment.FileName)
item = fmt.Sprintf(" %s %s", icon, attachment.FileName)
}
if m.deleteMode {
filename = fmt.Sprintf("%d%s", i, filename)
item = fmt.Sprintf("%d%s", i, item)
}
styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
styledAttachments = append(styledAttachments, attachmentStyles.Render(item))
}
content := lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...)
return content
Expand Down
1 change: 1 addition & 0 deletions internal/tui/components/dialogs/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type (
SwitchModelMsg struct{}
QuitMsg struct{}
OpenFilePickerMsg struct{}
OpenResourcePickerMsg struct{}
ToggleHelpMsg struct{}
ToggleCompactModeMsg struct{}
ToggleThinkingMsg struct{}
Expand Down
33 changes: 33 additions & 0 deletions internal/tui/components/dialogs/mcp/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package mcp

import (
"github.com/charmbracelet/bubbles/v2/key"
)

type KeyMap struct {
Close key.Binding
Select key.Binding
}

func DefaultKeyMap() KeyMap {
return KeyMap{
Close: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "close"),
),
Select: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select"),
),
}
}

func (k KeyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Select, k.Close}
}

func (k KeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Select, k.Close},
}
}
Loading
Loading