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
8 changes: 3 additions & 5 deletions go.mod
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line numberDiff line numberDiff line change
Expand Up@@ -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=
Expand Down
199 changes: 199 additions & 0 deletions internal/llm/agent/mcp-common.go
Original file line numberDiff line numberDiff line change
@@ -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
}
21 changes: 21 additions & 0 deletions internal/llm/agent/mcp-prompts.go
Original file line numberDiff line numberDiff line change
@@ -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
}
38 changes: 2 additions & 36 deletions internal/llm/agent/mcp-tools.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
}

Expand DownExpand Up@@ -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{}
Expand All@@ -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
}
15 changes: 15 additions & 0 deletions internal/tui/components/dialog/argument.go
Original file line numberDiff line numberDiff line change
@@ -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
2 changes: 2 additions & 0 deletions internal/tui/components/dialog/arguments.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
11 changes: 11 additions & 0 deletions internal/tui/components/dialog/mcp_prompt.go
Original file line numberDiff line numberDiff line change
@@ -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
}
Loading