diff --git a/go.mod b/go.mod index 4266e719be5e..050dd82b0026 100644 --- a/go.mod +++ b/go.mod @@ -15,13 +15,14 @@ require ( github.com/charmbracelet/bubbletea v1.3.4 github.com/charmbracelet/glamour v0.9.1 github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/log v0.4.2 github.com/charmbracelet/x/ansi v0.8.0 github.com/fsnotify/fsnotify v1.8.0 github.com/go-logfmt/logfmt v0.6.0 github.com/google/uuid v1.6.0 github.com/lithammer/fuzzysearch v1.1.8 github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231 - github.com/mark3labs/mcp-go v0.17.0 + github.com/mark3labs/mcp-go v0.28.0 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.16.0 @@ -34,10 +35,7 @@ require ( github.com/stretchr/testify v1.10.0 ) -require ( - github.com/charmbracelet/log v0.4.2 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect -) +require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect require ( cloud.google.com/go v0.116.0 // indirect diff --git a/go.sum b/go.sum index 83184ff79153..8092b51ed3f6 100644 --- a/go.sum +++ b/go.sum @@ -154,6 +154,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930= github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= +github.com/mark3labs/mcp-go v0.28.0 h1:7yl4y5D1KYU2f/9Uxp7xfLIggfunHoESCRbrjcytcLM= +github.com/mark3labs/mcp-go v0.28.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= diff --git a/internal/llm/agent/mcp-common.go b/internal/llm/agent/mcp-common.go new file mode 100644 index 000000000000..73949ce4d02c --- /dev/null +++ b/internal/llm/agent/mcp-common.go @@ -0,0 +1,199 @@ +package agent + +import ( + "context" + "fmt" + "log/slog" + + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" + "github.com/sst/opencode/internal/config" + "github.com/sst/opencode/internal/llm/tools" + "github.com/sst/opencode/internal/permission" + "github.com/sst/opencode/internal/version" +) + +// Global variables to store MCP resources +var ( + globalMCPTools []tools.BaseTool + mcpPrompts []MCPPrompt +) + +// GetMCPResources fetches both tools and prompts from all MCP servers +func GetMCPResources(ctx context.Context, permissions permission.Service) ([]tools.BaseTool, []MCPPrompt) { + // If already loaded, return cached values + if len(globalMCPTools) > 0 && len(mcpPrompts) > 0 { + return globalMCPTools, mcpPrompts + } + + // Clear existing resources + globalMCPTools = []tools.BaseTool{} + mcpPrompts = []MCPPrompt{} + + // Loop through all configured MCP servers + for serverName, serverConfig := range config.Get().MCPServers { + // Create a client for this server + c, err := createMCPClient(ctx, serverConfig) + if err != nil { + slog.Error("error creating MCP client", + "server", serverName, + "error", err) + continue + } + + // Get tools from this server + serverTools, err := fetchToolsFromClient(ctx, serverName, serverConfig, permissions, c) + if err != nil { + slog.Error("error fetching tools from MCP server", + "server", serverName, + "error", err) + } else { + globalMCPTools = append(globalMCPTools, serverTools...) + } + + // Get prompts from this server + serverPrompts, err := fetchPromptsFromClient(ctx, serverName, serverConfig, c) + if err != nil { + slog.Error("error fetching prompts from MCP server", + "server", serverName, + "error", err) + } else { + mcpPrompts = append(mcpPrompts, serverPrompts...) + } + + // Close the client + c.Close() + } + + return globalMCPTools, mcpPrompts +} + +// GetMcpTools returns all MCP tools +func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.BaseTool { + tools, _ := GetMCPResources(ctx, permissions) + return tools +} + +// GetMCPPrompts returns all MCP prompts +func GetMCPPrompts(ctx context.Context) []MCPPrompt { + _, prompts := GetMCPResources(ctx, nil) + return prompts +} + +// createMCPClient creates and initializes an MCP client for a server +func createMCPClient(ctx context.Context, serverConfig config.MCPServer) (MCPClient, error) { + var c MCPClient + var err error + + switch serverConfig.Type { + case config.MCPStdio: + c, err = client.NewStdioMCPClient( + serverConfig.Command, + serverConfig.Env, + serverConfig.Args..., + ) + case config.MCPSse: + c, err = client.NewSSEMCPClient( + serverConfig.URL, + client.WithHeaders(serverConfig.Headers), + ) + default: + return nil, fmt.Errorf("unsupported MCP server type: %s", serverConfig.Type) + } + + if err != nil { + return nil, fmt.Errorf("error creating MCP client: %w", err) + } + + // Initialize the client + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "OpenCode", + Version: version.Version, + } + + _, err = c.Initialize(ctx, initRequest) + if err != nil { + c.Close() + return nil, fmt.Errorf("error initializing MCP client: %w", err) + } + + return c, nil +} + +// fetchToolsFromClient fetches tools using an existing MCP client +func fetchToolsFromClient(ctx context.Context, serverName string, serverConfig config.MCPServer, permissions permission.Service, c MCPClient) ([]tools.BaseTool, error) { + var serverTools []tools.BaseTool + + // List tools + toolsRequest := mcp.ListToolsRequest{} + toolsResponse, err := c.ListTools(ctx, toolsRequest) + if err != nil { + return nil, fmt.Errorf("error listing tools: %w", err) + } + + // Create tool wrappers + for _, t := range toolsResponse.Tools { + serverTools = append(serverTools, NewMcpTool(serverName, t, permissions, serverConfig)) + } + + return serverTools, nil +} + +// fetchPromptsFromClient fetches prompts using an existing MCP client +func fetchPromptsFromClient(ctx context.Context, serverName string, serverConfig config.MCPServer, c MCPClient) ([]MCPPrompt, error) { + var serverPrompts []MCPPrompt + + // List prompts + promptsRequest := mcp.ListPromptsRequest{} + promptsResponse, err := c.ListPrompts(ctx, promptsRequest) + if err != nil { + return nil, fmt.Errorf("error listing prompts: %w", err) + } + + // Create prompt wrappers + for _, prompt := range promptsResponse.Prompts { + mcpPrompt := MCPPrompt{ + Name: prompt.Name, + Description: prompt.Description, + ServerName: serverName, + ServerConfig: serverConfig, + } + + for _, arg := range prompt.Arguments { + mcpPrompt.Arguments = append(mcpPrompt.Arguments, MCPPromptArgument{ + Name: arg.Name, + Description: arg.Description, + Required: arg.Required, + }) + } + + serverPrompts = append(serverPrompts, mcpPrompt) + } + + return serverPrompts, nil +} + +// ExecutePrompt executes a prompt on an MCP server +func ExecutePrompt(ctx context.Context, prompt MCPPrompt, args map[string]string) ([]mcp.PromptMessage, error) { + // Create a client for this server + c, err := createMCPClient(ctx, prompt.ServerConfig) + if err != nil { + return nil, fmt.Errorf("error creating MCP client: %w", err) + } + defer c.Close() + + // Get prompt + promptRequest := mcp.GetPromptRequest{} + promptRequest.Params.Name = prompt.Name + promptRequest.Params.Arguments = args + + promptResponse, err := c.GetPrompt(ctx, promptRequest) + if err != nil { + return nil, fmt.Errorf("error getting prompt: %w", err) + } + + // Return the full array of messages + return promptResponse.Messages, nil +} \ No newline at end of file diff --git a/internal/llm/agent/mcp-prompts.go b/internal/llm/agent/mcp-prompts.go new file mode 100644 index 000000000000..e243df9b8f3d --- /dev/null +++ b/internal/llm/agent/mcp-prompts.go @@ -0,0 +1,21 @@ +package agent + +import ( + "github.com/sst/opencode/internal/config" +) + +// MCPPrompt represents a prompt from an MCP server +type MCPPrompt struct { + Name string + Description string + Arguments []MCPPromptArgument + ServerName string + ServerConfig config.MCPServer +} + +// MCPPromptArgument represents an argument for an MCP prompt +type MCPPromptArgument struct { + Name string + Description string + Required bool +} diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go index 601fdf705c8c..beca57862fc6 100644 --- a/internal/llm/agent/mcp-tools.go +++ b/internal/llm/agent/mcp-tools.go @@ -29,6 +29,8 @@ type MCPClient interface { ) (*mcp.InitializeResult, error) ListTools(ctx context.Context, request mcp.ListToolsRequest) (*mcp.ListToolsResult, error) CallTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + ListPrompts(ctx context.Context, request mcp.ListPromptsRequest) (*mcp.ListPromptsResult, error) + GetPrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) Close() error } @@ -134,8 +136,6 @@ func NewMcpTool(name string, tool mcp.Tool, permissions permission.Service, mcpC } } -var mcpTools []tools.BaseTool - func getTools(ctx context.Context, name string, m config.MCPServer, permissions permission.Service, c MCPClient) []tools.BaseTool { var stdioTools []tools.BaseTool initRequest := mcp.InitializeRequest{} @@ -162,37 +162,3 @@ func getTools(ctx context.Context, name string, m config.MCPServer, permissions defer c.Close() return stdioTools } - -func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.BaseTool { - if len(mcpTools) > 0 { - return mcpTools - } - for name, m := range config.Get().MCPServers { - switch m.Type { - case config.MCPStdio: - c, err := client.NewStdioMCPClient( - m.Command, - m.Env, - m.Args..., - ) - if err != nil { - slog.Error("error creating mcp client", "error", err) - continue - } - - mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...) - case config.MCPSse: - c, err := client.NewSSEMCPClient( - m.URL, - client.WithHeaders(m.Headers), - ) - if err != nil { - slog.Error("error creating mcp client", "error", err) - continue - } - mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...) - } - } - - return mcpTools -} diff --git a/internal/tui/components/dialog/argument.go b/internal/tui/components/dialog/argument.go new file mode 100644 index 000000000000..9e239e0a8acc --- /dev/null +++ b/internal/tui/components/dialog/argument.go @@ -0,0 +1,15 @@ +package dialog + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +// Argument represents a command argument +type Argument struct { + Name string + Description string + Required bool +} + +// ArgumentHandler is a function that handles argument values +type ArgumentHandler func(values map[string]string) tea.Cmd diff --git a/internal/tui/components/dialog/arguments.go b/internal/tui/components/dialog/arguments.go index fed79bce3edf..047a0b2c66ea 100644 --- a/internal/tui/components/dialog/arguments.go +++ b/internal/tui/components/dialog/arguments.go @@ -41,6 +41,8 @@ type ShowMultiArgumentsDialogMsg struct { CommandID string Content string ArgNames []string + Arguments []Argument + Handler ArgumentHandler } // CloseMultiArgumentsDialogMsg is a message that is sent when the multi-arguments dialog is closed. diff --git a/internal/tui/components/dialog/mcp_prompt.go b/internal/tui/components/dialog/mcp_prompt.go new file mode 100644 index 000000000000..79c78a2c4e10 --- /dev/null +++ b/internal/tui/components/dialog/mcp_prompt.go @@ -0,0 +1,11 @@ +package dialog + +import ( + "github.com/sst/opencode/internal/llm/agent" +) + +// MCPPromptRunMsg is sent when an MCP prompt is executed +type MCPPromptRunMsg struct { + Prompt agent.MCPPrompt + Args map[string]string +} diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index 1b31c838c3ba..74b7c3df3483 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -2,7 +2,9 @@ package page import ( "context" + "encoding/json" "fmt" + "path/filepath" "strings" "github.com/charmbracelet/bubbles/key" @@ -10,6 +12,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/completions" + "github.com/sst/opencode/internal/llm/agent" "github.com/sst/opencode/internal/message" "github.com/sst/opencode/internal/session" "github.com/sst/opencode/internal/status" @@ -98,6 +101,93 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if cmd != nil { return p, cmd } + case dialog.MCPPromptRunMsg: + // Check if the agent is busy before executing MCP prompt commands + if p.app.PrimaryAgent.IsBusy() { + status.Warn("Agent is busy, please wait before executing a command...") + return p, nil + } + + // Execute the MCP prompt + messages, err := agent.ExecutePrompt(context.Background(), msg.Prompt, msg.Args) + if err != nil { + status.Error(fmt.Sprintf("Failed to execute MCP prompt: %v", err)) + return p, nil + } + + // Process messages to extract text and resources + var textContent strings.Builder + var attachments []message.Attachment + + for _, msg := range messages { + if msg.Role == "user" { + // Try to extract content based on JSON structure + contentJSON, err := json.Marshal(msg.Content) + if err != nil { + continue + } + + var contentMap map[string]interface{} + if err := json.Unmarshal(contentJSON, &contentMap); err != nil { + continue + } + + contentType, hasType := contentMap["type"].(string) + if !hasType { + continue + } + + if contentType == "text" { + // Handle text content + if text, ok := contentMap["text"].(string); ok { + textContent.WriteString(text) + textContent.WriteString("\n\n") + } + } else if contentType == "resource" { + // Handle resource content + resourceJSON, err := json.Marshal(contentMap["resource"]) + if err != nil { + continue + } + + var resourceMap map[string]interface{} + if err := json.Unmarshal(resourceJSON, &resourceMap); err != nil { + continue + } + + uri, hasURI := resourceMap["uri"].(string) + text, hasText := resourceMap["text"].(string) + mimeType, hasMimeType := resourceMap["mimeType"].(string) + + if hasURI { + // Add a reference to the resource in the text + textContent.WriteString(fmt.Sprintf("Resource: %s\n\n", uri)) + + // Create an attachment for the resource + if hasText { + attachment := message.Attachment{ + FileName: filepath.Base(uri), + MimeType: "text/plain", // Default mime type + Content: []byte(text), + } + + // Set mime type if available + if hasMimeType { + attachment.MimeType = mimeType + } + + attachments = append(attachments, attachment) + } + } + } + } + } + + // Send the prompt text as a message with attachments + cmd := p.sendMessage(textContent.String(), attachments) + if cmd != nil { + return p, cmd + } case state.SessionSelectedMsg: cmd := p.setSidebar() cmds = append(cmds, cmd) diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 56be0461970d..f82e9d870668 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -2,8 +2,10 @@ package tui import ( "context" + "encoding/json" "fmt" "log/slog" + "path/filepath" "strings" "github.com/charmbracelet/bubbles/cursor" @@ -83,7 +85,7 @@ var keys = keyMap{ key.WithKeys("ctrl+t"), key.WithHelp("ctrl+t", "switch theme"), ), - + Tools: key.NewBinding( key.WithKeys("f9"), key.WithHelp("f9", "show available tools"), @@ -144,7 +146,8 @@ type appModel struct { showMultiArgumentsDialog bool multiArgumentsDialog dialog.MultiArgumentsDialogCmp - + multiArgumentsHandler dialog.ArgumentHandler + showToolsDialog bool toolsDialog dialog.ToolsDialog } @@ -299,11 +302,11 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case dialog.CloseThemeDialogMsg: a.showThemeDialog = false return a, nil - + case dialog.CloseToolsDialogMsg: a.showToolsDialog = false return a, nil - + case dialog.ShowToolsDialogMsg: a.showToolsDialog = msg.Show return a, nil @@ -368,16 +371,35 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case dialog.ShowMultiArgumentsDialogMsg: // Show multi-arguments dialog - a.multiArgumentsDialog = dialog.NewMultiArgumentsDialogCmp(msg.CommandID, msg.Content, msg.ArgNames) - a.showMultiArgumentsDialog = true + if len(msg.ArgNames) > 0 { + a.multiArgumentsDialog = dialog.NewMultiArgumentsDialogCmp(msg.CommandID, msg.Content, msg.ArgNames) + a.showMultiArgumentsDialog = true + a.multiArgumentsHandler = nil // Clear any previous handler + } else if len(msg.Arguments) > 0 { + // Extract argument names from Arguments + argNames := make([]string, len(msg.Arguments)) + for i, arg := range msg.Arguments { + argNames[i] = arg.Name + } + a.multiArgumentsDialog = dialog.NewMultiArgumentsDialogCmp(msg.CommandID, "", argNames) + a.showMultiArgumentsDialog = true + // Store the handler for later use + a.multiArgumentsHandler = msg.Handler + } return a, a.multiArgumentsDialog.Init() case dialog.CloseMultiArgumentsDialogMsg: // Close multi-arguments dialog a.showMultiArgumentsDialog = false - // If submitted, replace all named arguments and run the command + // If submitted, handle the arguments if msg.Submit { + // If we have a custom handler, use it + if a.multiArgumentsHandler != nil { + return a, a.multiArgumentsHandler(msg.Args) + } + + // Otherwise, use the traditional approach for custom commands content := msg.Content // Replace each named argument with its value @@ -436,7 +458,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showThemeDialog = false a.showModelDialog = false a.showFilepicker = false - + // Load sessions and show the dialog sessions, err := a.app.Sessions.List(context.Background()) if err != nil { @@ -457,7 +479,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Close other dialogs a.showToolsDialog = false a.showModelDialog = false - + // Show commands dialog if len(a.commands) == 0 { status.Warn("No commands available") @@ -478,7 +500,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showToolsDialog = false a.showThemeDialog = false a.showFilepicker = false - + a.showModelDialog = true return a, nil } @@ -489,17 +511,17 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showToolsDialog = false a.showModelDialog = false a.showFilepicker = false - + a.showThemeDialog = true return a, a.themeDialog.Init() } return a, nil case key.Matches(msg, keys.Tools): // Check if any other dialog is open - if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && - !a.showSessionDialog && !a.showCommandDialog && !a.showThemeDialog && - !a.showFilepicker && !a.showModelDialog && !a.showInitDialog && - !a.showMultiArgumentsDialog { + if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && + !a.showSessionDialog && !a.showCommandDialog && !a.showThemeDialog && + !a.showFilepicker && !a.showModelDialog && !a.showInitDialog && + !a.showMultiArgumentsDialog { // Toggle tools dialog a.showToolsDialog = !a.showToolsDialog if a.showToolsDialog { @@ -555,7 +577,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, nil } a.showHelp = !a.showHelp - + // Close other dialogs if opening help if a.showHelp { a.showToolsDialog = false @@ -574,7 +596,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showFilepicker = !a.showFilepicker a.filepicker.ToggleFilepicker(a.showFilepicker) a.app.SetFilepickerOpen(a.showFilepicker) - + // Close other dialogs if opening filepicker if a.showFilepicker { a.showToolsDialog = false @@ -681,7 +703,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, tea.Batch(cmds...) } } - + if a.showToolsDialog { d, toolsCmd := a.toolsDialog.Update(msg) a.toolsDialog = d.(dialog.ToolsDialog) @@ -706,6 +728,138 @@ func (a *appModel) RegisterCommand(cmd dialog.Command) { a.commands = append(a.commands, cmd) } +// RegisterMCPPrompts registers all MCP prompts as commands +func (a *appModel) RegisterMCPPrompts(ctx context.Context) { + prompts := agent.GetMCPPrompts(ctx) + for _, prompt := range prompts { + // Create a copy of the prompt for the closure + p := prompt + + // Create command ID in the format : + commandID := fmt.Sprintf("%s:%s", p.ServerName, p.Name) + + // Create command + cmd := dialog.Command{ + ID: commandID, + Title: commandID, + Description: p.Description, + Handler: func(cmd dialog.Command) tea.Cmd { + // If the prompt has arguments, show the arguments dialog + if len(p.Arguments) > 0 { + // Convert MCPPromptArgument to dialog.Argument + var args []dialog.Argument + for _, arg := range p.Arguments { + args = append(args, dialog.Argument{ + Name: arg.Name, + Description: arg.Description, + Required: arg.Required, + }) + } + + return util.CmdHandler(dialog.ShowMultiArgumentsDialogMsg{ + CommandID: cmd.ID, + Arguments: args, + Handler: func(values map[string]string) tea.Cmd { + return a.executeMCPPrompt(p, values) + }, + }) + } + + // No arguments, execute directly + return a.executeMCPPrompt(p, nil) + }, + } + + a.RegisterCommand(cmd) + } +} + +// executeMCPPrompt executes an MCP prompt and sends the result as a message +func (a *appModel) executeMCPPrompt(prompt agent.MCPPrompt, args map[string]string) tea.Cmd { + return func() tea.Msg { + // Execute the prompt + messages, err := agent.ExecutePrompt(context.Background(), prompt, args) + if err != nil { + status.Error(fmt.Sprintf("Failed to execute prompt: %v", err)) + return nil + } + + // Process messages to extract text and resources + var textContent strings.Builder + var attachments []message.Attachment + + for _, msg := range messages { + if msg.Role == "user" { + // Try to extract content based on JSON structure + contentJSON, err := json.Marshal(msg.Content) + if err != nil { + continue + } + + var contentMap map[string]interface{} + if err := json.Unmarshal(contentJSON, &contentMap); err != nil { + continue + } + + contentType, hasType := contentMap["type"].(string) + if !hasType { + continue + } + + if contentType == "text" { + // Handle text content + if text, ok := contentMap["text"].(string); ok { + textContent.WriteString(text) + textContent.WriteString("\n\n") + } + } else if contentType == "resource" { + // Handle resource content + resourceJSON, err := json.Marshal(contentMap["resource"]) + if err != nil { + continue + } + + var resourceMap map[string]interface{} + if err := json.Unmarshal(resourceJSON, &resourceMap); err != nil { + continue + } + + uri, hasURI := resourceMap["uri"].(string) + text, hasText := resourceMap["text"].(string) + mimeType, hasMimeType := resourceMap["mimeType"].(string) + + if hasURI { + // Add a reference to the resource in the text + textContent.WriteString(fmt.Sprintf("Resource: %s\n\n", uri)) + + // Create an attachment for the resource + if hasText { + attachment := message.Attachment{ + FileName: filepath.Base(uri), + MimeType: "text/plain", // Default mime type + Content: []byte(text), + } + + // Set mime type if available + if hasMimeType { + attachment.MimeType = mimeType + } + + attachments = append(attachments, attachment) + } + } + } + } + } + + // Send the result as a message with attachments + return chat.SendMsg{ + Text: textContent.String(), + Attachments: attachments, + } + } +} + // getAvailableToolNames returns a list of all available tool names func getAvailableToolNames(app *app.App) []string { // Get primary agent tools (which already include MCP tools) @@ -716,13 +870,13 @@ func getAvailableToolNames(app *app.App) []string { app.History, app.LSPClients, ) - + // Extract tool names var toolNames []string for _, tool := range allTools { toolNames = append(toolNames, tool.Info().Name) } - + return toolNames } @@ -931,7 +1085,7 @@ func (a appModel) View() string { true, ) } - + if a.showToolsDialog { overlay := a.toolsDialog.View() row := lipgloss.Height(appView) / 2 @@ -1021,5 +1175,7 @@ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules ( } } + model.RegisterMCPPrompts(context.Background()) + return model }