From e12526c5d4c89909acd2ff81c852cd78771d873e Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sun, 18 May 2025 15:02:58 +0300 Subject: [PATCH 1/5] Add prompts from MCP servers as commands --- go.mod | 6 +- go.sum | 5 +- internal/llm/agent/mcp-prompts.go | 190 +++++++++++++++++++ internal/tui/components/dialog/argument.go | 15 ++ internal/tui/components/dialog/arguments.go | 2 + internal/tui/components/dialog/mcp_prompt.go | 11 ++ internal/tui/page/chat.go | 20 ++ internal/tui/tui.go | 91 ++++++++- 8 files changed, 329 insertions(+), 11 deletions(-) create mode 100644 internal/llm/agent/mcp-prompts.go create mode 100644 internal/tui/components/dialog/argument.go create mode 100644 internal/tui/components/dialog/mcp_prompt.go diff --git a/go.mod b/go.mod index ae1a7f09d5ec..8fbf4c839caa 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ 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 @@ -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 9c758aefe9ef..2b622f3daa75 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,6 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= -github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= @@ -86,9 +84,8 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= diff --git a/internal/llm/agent/mcp-prompts.go b/internal/llm/agent/mcp-prompts.go new file mode 100644 index 000000000000..cb1f59447287 --- /dev/null +++ b/internal/llm/agent/mcp-prompts.go @@ -0,0 +1,190 @@ +package agent + +import ( + "context" + "encoding/json" + "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/version" +) + +// 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 +} + +// GetMCPPrompts fetches all prompts from all registered MCP servers +func GetMCPPrompts(ctx context.Context) []MCPPrompt { + var prompts []MCPPrompt + + for serverName, serverConfig := range config.Get().MCPServers { + serverPrompts, err := getPromptsFromServer(ctx, serverName, serverConfig) + if err != nil { + slog.Error("error fetching prompts from MCP server", + "server", serverName, + "error", err) + continue + } + prompts = append(prompts, serverPrompts...) + } + + return prompts +} + +// getPromptsFromServer fetches prompts from a specific MCP server +func getPromptsFromServer(ctx context.Context, serverName string, serverConfig config.MCPServer) ([]MCPPrompt, error) { + var c client.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) + } + defer c.Close() + + // 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 { + return nil, fmt.Errorf("error initializing MCP client: %w", err) + } + + // List prompts + promptsRequest := mcp.ListPromptsRequest{} + promptsResponse, err := c.ListPrompts(ctx, promptsRequest) + if err != nil { + return nil, fmt.Errorf("error listing prompts: %w", err) + } + + var result []MCPPrompt + 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, + }) + } + + result = append(result, mcpPrompt) + } + + return result, nil +} + +// ExecutePrompt executes a prompt on an MCP server +func ExecutePrompt(ctx context.Context, prompt MCPPrompt, args map[string]string) (string, error) { + var c client.MCPClient + var err error + + switch prompt.ServerConfig.Type { + case config.MCPStdio: + c, err = client.NewStdioMCPClient( + prompt.ServerConfig.Command, + prompt.ServerConfig.Env, + prompt.ServerConfig.Args..., + ) + case config.MCPSse: + c, err = client.NewSSEMCPClient( + prompt.ServerConfig.URL, + client.WithHeaders(prompt.ServerConfig.Headers), + ) + default: + return "", fmt.Errorf("unsupported MCP server type: %s", prompt.ServerConfig.Type) + } + + if err != nil { + return "", fmt.Errorf("error creating MCP client: %w", err) + } + defer c.Close() + + // 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 { + return "", fmt.Errorf("error initializing MCP client: %w", err) + } + + // Convert string args to any map + promptArgs := make(map[string]any) + for k, v := range args { + promptArgs[k] = v + } + + // Get prompt + promptRequest := mcp.GetPromptRequest{} + promptRequest.Params.Name = prompt.Name + promptRequest.Params.Arguments = args + + promptResponse, err := c.GetPrompt(ctx, promptRequest) + if err != nil { + return "", fmt.Errorf("error getting prompt: %w", err) + } + + // Convert messages to a string + var result string + for _, msg := range promptResponse.Messages { + if msg.Role == "user" { + if content, ok := msg.Content.(mcp.TextContent); ok { + result += content.Text + "\n\n" + } else { + // Try to marshal the content to JSON + contentBytes, err := json.Marshal(msg.Content) + if err == nil { + result += string(contentBytes) + "\n\n" + } + } + } + } + + return result, nil +} \ No newline at end of file diff --git a/internal/tui/components/dialog/argument.go b/internal/tui/components/dialog/argument.go new file mode 100644 index 000000000000..7276c9fd7164 --- /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 \ No newline at end of file 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..22c555e57354 --- /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 +} \ No newline at end of file diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index 1b31c838c3ba..d2c3d4325c82 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -10,6 +10,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 +99,25 @@ 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 + promptText, 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 + } + + // Send the prompt text as a message + cmd := p.sendMessage(promptText, nil) + 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 299c697939c9..fd0c5533600d 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -144,6 +144,7 @@ type appModel struct { showMultiArgumentsDialog bool multiArgumentsDialog dialog.MultiArgumentsDialogCmp + multiArgumentsHandler dialog.ArgumentHandler showToolsDialog bool toolsDialog dialog.ToolsDialog @@ -368,16 +369,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 @@ -703,6 +723,69 @@ 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 + result, err := agent.ExecutePrompt(context.Background(), prompt, args) + if err != nil { + status.Error(fmt.Sprintf("Failed to execute prompt: %v", err)) + return nil + } + + // Send the result as a message + return chat.SendMsg{ + Text: result, + } + } +} + // getAvailableToolNames returns a list of all available tool names func getAvailableToolNames(app *app.App) []string { // Get primary agent tools (which already include MCP tools) @@ -1018,5 +1101,7 @@ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules ( } } + model.RegisterMCPPrompts(context.Background()) + return model } From e2eb9584532edd683833d67bef712430c0f649d9 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sun, 18 May 2025 15:17:22 +0300 Subject: [PATCH 2/5] Parse messages returned from prompt request correctly --- internal/llm/agent/mcp-prompts.go | 30 +++--------- internal/tui/page/chat.go | 76 ++++++++++++++++++++++++++++-- internal/tui/tui.go | 77 +++++++++++++++++++++++++++++-- 3 files changed, 154 insertions(+), 29 deletions(-) diff --git a/internal/llm/agent/mcp-prompts.go b/internal/llm/agent/mcp-prompts.go index cb1f59447287..faf9ea901037 100644 --- a/internal/llm/agent/mcp-prompts.go +++ b/internal/llm/agent/mcp-prompts.go @@ -2,7 +2,6 @@ package agent import ( "context" - "encoding/json" "fmt" "log/slog" @@ -116,7 +115,7 @@ func getPromptsFromServer(ctx context.Context, serverName string, serverConfig c } // ExecutePrompt executes a prompt on an MCP server -func ExecutePrompt(ctx context.Context, prompt MCPPrompt, args map[string]string) (string, error) { +func ExecutePrompt(ctx context.Context, prompt MCPPrompt, args map[string]string) ([]mcp.PromptMessage, error) { var c client.MCPClient var err error @@ -133,11 +132,11 @@ func ExecutePrompt(ctx context.Context, prompt MCPPrompt, args map[string]string client.WithHeaders(prompt.ServerConfig.Headers), ) default: - return "", fmt.Errorf("unsupported MCP server type: %s", prompt.ServerConfig.Type) + return nil, fmt.Errorf("unsupported MCP server type: %s", prompt.ServerConfig.Type) } if err != nil { - return "", fmt.Errorf("error creating MCP client: %w", err) + return nil, fmt.Errorf("error creating MCP client: %w", err) } defer c.Close() @@ -151,7 +150,7 @@ func ExecutePrompt(ctx context.Context, prompt MCPPrompt, args map[string]string _, err = c.Initialize(ctx, initRequest) if err != nil { - return "", fmt.Errorf("error initializing MCP client: %w", err) + return nil, fmt.Errorf("error initializing MCP client: %w", err) } // Convert string args to any map @@ -167,24 +166,9 @@ func ExecutePrompt(ctx context.Context, prompt MCPPrompt, args map[string]string promptResponse, err := c.GetPrompt(ctx, promptRequest) if err != nil { - return "", fmt.Errorf("error getting prompt: %w", err) - } - - // Convert messages to a string - var result string - for _, msg := range promptResponse.Messages { - if msg.Role == "user" { - if content, ok := msg.Content.(mcp.TextContent); ok { - result += content.Text + "\n\n" - } else { - // Try to marshal the content to JSON - contentBytes, err := json.Marshal(msg.Content) - if err == nil { - result += string(contentBytes) + "\n\n" - } - } - } + return nil, fmt.Errorf("error getting prompt: %w", err) } - return result, nil + // Return the full array of messages + return promptResponse.Messages, nil } \ No newline at end of file diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index d2c3d4325c82..d9bfa325e01d 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" @@ -107,14 +109,82 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Execute the MCP prompt - promptText, err := agent.ExecutePrompt(context.Background(), msg.Prompt, msg.Args) + 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 } - // Send the prompt text as a message - cmd := p.sendMessage(promptText, 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 } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index fd0c5533600d..b80805cf2e15 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" @@ -773,15 +775,84 @@ func (a *appModel) RegisterMCPPrompts(ctx context.Context) { func (a *appModel) executeMCPPrompt(prompt agent.MCPPrompt, args map[string]string) tea.Cmd { return func() tea.Msg { // Execute the prompt - result, err := agent.ExecutePrompt(context.Background(), prompt, args) + messages, err := agent.ExecutePrompt(context.Background(), prompt, args) if err != nil { status.Error(fmt.Sprintf("Failed to execute prompt: %v", err)) return nil } - // Send the result as a message + // 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: result, + Text: textContent.String(), + Attachments: attachments, } } } From 054b8827b486aa921fae5c22d9f42dbfad24f3a9 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sun, 18 May 2025 15:21:05 +0300 Subject: [PATCH 3/5] feat: implement MCP prompts support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for Model Context Protocol (MCP) prompts, which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs. They provide a powerful way to standardize and share common LLM interactions. Key changes: - Fetch prompts from MCP servers - Register prompts as commands with format `:` - Create dialog for prompt arguments - Process structured responses including resources - Send formatted messages with attachments 🤖 Generated with opencode Co-Authored-By: opencode --- internal/llm/agent/mcp-prompts.go | 20 +++--- internal/tui/components/dialog/argument.go | 2 +- internal/tui/components/dialog/mcp_prompt.go | 2 +- internal/tui/page/chat.go | 20 +++--- internal/tui/tui.go | 72 ++++++++++---------- 5 files changed, 58 insertions(+), 58 deletions(-) diff --git a/internal/llm/agent/mcp-prompts.go b/internal/llm/agent/mcp-prompts.go index faf9ea901037..c099bdac6f6a 100644 --- a/internal/llm/agent/mcp-prompts.go +++ b/internal/llm/agent/mcp-prompts.go @@ -13,10 +13,10 @@ import ( // MCPPrompt represents a prompt from an MCP server type MCPPrompt struct { - Name string - Description string - Arguments []MCPPromptArgument - ServerName string + Name string + Description string + Arguments []MCPPromptArgument + ServerName string ServerConfig config.MCPServer } @@ -34,8 +34,8 @@ func GetMCPPrompts(ctx context.Context) []MCPPrompt { for serverName, serverConfig := range config.Get().MCPServers { serverPrompts, err := getPromptsFromServer(ctx, serverName, serverConfig) if err != nil { - slog.Error("error fetching prompts from MCP server", - "server", serverName, + slog.Error("error fetching prompts from MCP server", + "server", serverName, "error", err) continue } @@ -94,9 +94,9 @@ func getPromptsFromServer(ctx context.Context, serverName string, serverConfig c var result []MCPPrompt for _, prompt := range promptsResponse.Prompts { mcpPrompt := MCPPrompt{ - Name: prompt.Name, - Description: prompt.Description, - ServerName: serverName, + Name: prompt.Name, + Description: prompt.Description, + ServerName: serverName, ServerConfig: serverConfig, } @@ -171,4 +171,4 @@ func ExecutePrompt(ctx context.Context, prompt MCPPrompt, args map[string]string // Return the full array of messages return promptResponse.Messages, nil -} \ No newline at end of file +} diff --git a/internal/tui/components/dialog/argument.go b/internal/tui/components/dialog/argument.go index 7276c9fd7164..9e239e0a8acc 100644 --- a/internal/tui/components/dialog/argument.go +++ b/internal/tui/components/dialog/argument.go @@ -12,4 +12,4 @@ type Argument struct { } // ArgumentHandler is a function that handles argument values -type ArgumentHandler func(values map[string]string) tea.Cmd \ No newline at end of file +type ArgumentHandler func(values map[string]string) tea.Cmd diff --git a/internal/tui/components/dialog/mcp_prompt.go b/internal/tui/components/dialog/mcp_prompt.go index 22c555e57354..79c78a2c4e10 100644 --- a/internal/tui/components/dialog/mcp_prompt.go +++ b/internal/tui/components/dialog/mcp_prompt.go @@ -8,4 +8,4 @@ import ( type MCPPromptRunMsg struct { Prompt agent.MCPPrompt Args map[string]string -} \ No newline at end of file +} diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index d9bfa325e01d..74b7c3df3483 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -118,7 +118,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // 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 @@ -126,17 +126,17 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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 { @@ -149,20 +149,20 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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{ @@ -170,12 +170,12 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { MimeType: "text/plain", // Default mime type Content: []byte(text), } - + // Set mime type if available if hasMimeType { attachment.MimeType = mimeType } - + attachments = append(attachments, attachment) } } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index b80805cf2e15..329669e6ef23 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -85,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"), @@ -147,7 +147,7 @@ type appModel struct { showMultiArgumentsDialog bool multiArgumentsDialog dialog.MultiArgumentsDialogCmp multiArgumentsHandler dialog.ArgumentHandler - + showToolsDialog bool toolsDialog dialog.ToolsDialog } @@ -302,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 @@ -398,7 +398,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if a.multiArgumentsHandler != nil { return a, a.multiArgumentsHandler(msg.Args) } - + // Otherwise, use the traditional approach for custom commands content := msg.Content @@ -457,7 +457,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 { @@ -478,7 +478,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") @@ -499,7 +499,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 } @@ -510,17 +510,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 { @@ -575,7 +575,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 @@ -593,7 +593,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Toggle filepicker a.showFilepicker = !a.showFilepicker a.filepicker.ToggleFilepicker(a.showFilepicker) - + // Close other dialogs if opening filepicker if a.showFilepicker { a.showToolsDialog = false @@ -700,7 +700,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) @@ -731,10 +731,10 @@ func (a *appModel) RegisterMCPPrompts(ctx context.Context) { 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, @@ -752,7 +752,7 @@ func (a *appModel) RegisterMCPPrompts(ctx context.Context) { Required: arg.Required, }) } - + return util.CmdHandler(dialog.ShowMultiArgumentsDialogMsg{ CommandID: cmd.ID, Arguments: args, @@ -761,12 +761,12 @@ func (a *appModel) RegisterMCPPrompts(ctx context.Context) { }, }) } - + // No arguments, execute directly return a.executeMCPPrompt(p, nil) }, } - + a.RegisterCommand(cmd) } } @@ -780,11 +780,11 @@ func (a *appModel) executeMCPPrompt(prompt agent.MCPPrompt, args map[string]stri 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 @@ -792,17 +792,17 @@ func (a *appModel) executeMCPPrompt(prompt agent.MCPPrompt, args map[string]stri 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 { @@ -815,20 +815,20 @@ func (a *appModel) executeMCPPrompt(prompt agent.MCPPrompt, args map[string]stri 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{ @@ -836,19 +836,19 @@ func (a *appModel) executeMCPPrompt(prompt agent.MCPPrompt, args map[string]stri 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(), @@ -867,13 +867,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 } @@ -1082,7 +1082,7 @@ func (a appModel) View() string { true, ) } - + if a.showToolsDialog { overlay := a.toolsDialog.View() row := lipgloss.Height(appView) / 2 From a5aed606907cb9b67ab21f49960b0e2e5d9fc820 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sun, 18 May 2025 15:22:52 +0300 Subject: [PATCH 4/5] chore: update mark3labs/mcp-go to latest version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the MCP Go SDK from v0.17.0 to v0.28.0 to ensure compatibility with the latest MCP protocol features. 🤖 Generated with opencode Co-Authored-By: opencode --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 8fbf4c839caa..df4933117e3a 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( 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 diff --git a/go.sum b/go.sum index 2b622f3daa75..50198b6ea6cd 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= From aad4791c3656b164a7a34e5653d2aa41d22caf49 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sun, 18 May 2025 16:30:05 +0300 Subject: [PATCH 5/5] refactor: reuse MCP client for tools and prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors the MCP client handling to reuse the same client for fetching both tools and prompts from each server. This improves efficiency by: - Creating a single client per server - Fetching both tools and prompts in one pass - Using a shared initialization process - Properly managing client lifecycle 🤖 Generated with opencode Co-Authored-By: opencode --- internal/llm/agent/mcp-common.go | 199 ++++++++++++++++++++++++++++++ internal/llm/agent/mcp-prompts.go | 153 ----------------------- internal/llm/agent/mcp-tools.go | 38 +----- 3 files changed, 201 insertions(+), 189 deletions(-) create mode 100644 internal/llm/agent/mcp-common.go 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 index c099bdac6f6a..e243df9b8f3d 100644 --- a/internal/llm/agent/mcp-prompts.go +++ b/internal/llm/agent/mcp-prompts.go @@ -1,14 +1,7 @@ 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/version" ) // MCPPrompt represents a prompt from an MCP server @@ -26,149 +19,3 @@ type MCPPromptArgument struct { Description string Required bool } - -// GetMCPPrompts fetches all prompts from all registered MCP servers -func GetMCPPrompts(ctx context.Context) []MCPPrompt { - var prompts []MCPPrompt - - for serverName, serverConfig := range config.Get().MCPServers { - serverPrompts, err := getPromptsFromServer(ctx, serverName, serverConfig) - if err != nil { - slog.Error("error fetching prompts from MCP server", - "server", serverName, - "error", err) - continue - } - prompts = append(prompts, serverPrompts...) - } - - return prompts -} - -// getPromptsFromServer fetches prompts from a specific MCP server -func getPromptsFromServer(ctx context.Context, serverName string, serverConfig config.MCPServer) ([]MCPPrompt, error) { - var c client.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) - } - defer c.Close() - - // 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 { - return nil, fmt.Errorf("error initializing MCP client: %w", err) - } - - // List prompts - promptsRequest := mcp.ListPromptsRequest{} - promptsResponse, err := c.ListPrompts(ctx, promptsRequest) - if err != nil { - return nil, fmt.Errorf("error listing prompts: %w", err) - } - - var result []MCPPrompt - 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, - }) - } - - result = append(result, mcpPrompt) - } - - return result, nil -} - -// ExecutePrompt executes a prompt on an MCP server -func ExecutePrompt(ctx context.Context, prompt MCPPrompt, args map[string]string) ([]mcp.PromptMessage, error) { - var c client.MCPClient - var err error - - switch prompt.ServerConfig.Type { - case config.MCPStdio: - c, err = client.NewStdioMCPClient( - prompt.ServerConfig.Command, - prompt.ServerConfig.Env, - prompt.ServerConfig.Args..., - ) - case config.MCPSse: - c, err = client.NewSSEMCPClient( - prompt.ServerConfig.URL, - client.WithHeaders(prompt.ServerConfig.Headers), - ) - default: - return nil, fmt.Errorf("unsupported MCP server type: %s", prompt.ServerConfig.Type) - } - - if err != nil { - return nil, fmt.Errorf("error creating MCP client: %w", err) - } - defer c.Close() - - // 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 { - return nil, fmt.Errorf("error initializing MCP client: %w", err) - } - - // Convert string args to any map - promptArgs := make(map[string]any) - for k, v := range args { - promptArgs[k] = v - } - - // 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 -} 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 -}