chore: some mcp improvements

Kujtim Hoxha created

Change summary

internal/config/config.go                                  | 45 ++++++-
internal/llm/agent/mcp-tools.go                            | 20 +-
internal/tui/components/dialogs/permissions/permissions.go | 25 +++-
3 files changed, 69 insertions(+), 21 deletions(-)

Detailed changes

internal/config/config.go 🔗

@@ -6,8 +6,10 @@ import (
 	"slices"
 	"strings"
 
+	"github.com/charmbracelet/crush/internal/env"
 	"github.com/charmbracelet/crush/internal/fur/provider"
 	"github.com/tidwall/sjson"
+	"golang.org/x/exp/slog"
 )
 
 const (
@@ -90,12 +92,12 @@ const (
 )
 
 type MCPConfig struct {
-	Command  string   `json:"command,omitempty" `
-	Env      []string `json:"env,omitempty"`
-	Args     []string `json:"args,omitempty"`
-	Type     MCPType  `json:"type"`
-	URL      string   `json:"url,omitempty"`
-	Disabled bool     `json:"disabled,omitempty"`
+	Command  string            `json:"command,omitempty" `
+	Env      map[string]string `json:"env,omitempty"`
+	Args     []string          `json:"args,omitempty"`
+	Type     MCPType           `json:"type"`
+	URL      string            `json:"url,omitempty"`
+	Disabled bool              `json:"disabled,omitempty"`
 
 	// TODO: maybe make it possible to get the value from the env
 	Headers map[string]string `json:"headers,omitempty"`
@@ -165,6 +167,37 @@ func (l LSPs) Sorted() []LSP {
 	return sorted
 }
 
+func (m MCPConfig) ResolvedEnv() []string {
+	resolver := NewShellVariableResolver(env.New())
+	for e, v := range m.Env {
+		var err error
+		m.Env[e], err = resolver.ResolveValue(v)
+		if err != nil {
+			slog.Error("error resolving environment variable", "error", err, "variable", e, "value", v)
+			continue
+		}
+	}
+
+	env := make([]string, 0, len(m.Env))
+	for k, v := range m.Env {
+		env = append(env, fmt.Sprintf("%s=%s", k, v))
+	}
+	return env
+}
+
+func (m MCPConfig) ResolvedHeaders() map[string]string {
+	resolver := NewShellVariableResolver(env.New())
+	for e, v := range m.Headers {
+		var err error
+		m.Headers[e], err = resolver.ResolveValue(v)
+		if err != nil {
+			slog.Error("error resolving header variable", "error", err, "variable", e, "value", v)
+			continue
+		}
+	}
+	return m.Headers
+}
+
 type Agent struct {
 	ID          string `json:"id,omitempty"`
 	Name        string `json:"name,omitempty"`

internal/llm/agent/mcp-tools.go 🔗

@@ -36,7 +36,7 @@ type MCPClient interface {
 }
 
 func (b *mcpTool) Name() string {
-	return fmt.Sprintf("%s_%s", b.mcpName, b.tool.Name)
+	return fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name)
 }
 
 func (b *mcpTool) Info() tools.ToolInfo {
@@ -45,7 +45,7 @@ func (b *mcpTool) Info() tools.ToolInfo {
 		required = make([]string, 0)
 	}
 	return tools.ToolInfo{
-		Name:        fmt.Sprintf("%s_%s", b.mcpName, b.tool.Name),
+		Name:        fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name),
 		Description: b.tool.Description,
 		Parameters:  b.tool.InputSchema.Properties,
 		Required:    required,
@@ -107,14 +107,14 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
 		},
 	)
 	if !p {
-		return tools.NewTextErrorResponse("permission denied"), nil
+		return tools.ToolResponse{}, permission.ErrorPermissionDenied
 	}
 
 	switch b.mcpConfig.Type {
 	case config.MCPStdio:
 		c, err := client.NewStdioMCPClient(
 			b.mcpConfig.Command,
-			b.mcpConfig.Env,
+			b.mcpConfig.ResolvedEnv(),
 			b.mcpConfig.Args...,
 		)
 		if err != nil {
@@ -124,7 +124,7 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
 	case config.MCPHttp:
 		c, err := client.NewStreamableHttpClient(
 			b.mcpConfig.URL,
-			transport.WithHTTPHeaders(b.mcpConfig.Headers),
+			transport.WithHTTPHeaders(b.mcpConfig.ResolvedHeaders()),
 		)
 		if err != nil {
 			return tools.NewTextErrorResponse(err.Error()), nil
@@ -133,7 +133,7 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
 	case config.MCPSse:
 		c, err := client.NewSSEMCPClient(
 			b.mcpConfig.URL,
-			client.WithHeaders(b.mcpConfig.Headers),
+			client.WithHeaders(b.mcpConfig.ResolvedHeaders()),
 		)
 		if err != nil {
 			return tools.NewTextErrorResponse(err.Error()), nil
@@ -192,11 +192,12 @@ func GetMcpTools(ctx context.Context, permissions permission.Service, cfg *confi
 			slog.Debug("skipping disabled mcp", "name", name)
 			continue
 		}
+
 		switch m.Type {
 		case config.MCPStdio:
 			c, err := client.NewStdioMCPClient(
 				m.Command,
-				m.Env,
+				m.ResolvedEnv(),
 				m.Args...,
 			)
 			if err != nil {
@@ -206,9 +207,10 @@ func GetMcpTools(ctx context.Context, permissions permission.Service, cfg *confi
 
 			mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c, cfg.WorkingDir())...)
 		case config.MCPHttp:
+			slog.Info("creating mcp client", "name", name, "url", m.URL, "headers", m.ResolvedHeaders())
 			c, err := client.NewStreamableHttpClient(
 				m.URL,
-				transport.WithHTTPHeaders(m.Headers),
+				transport.WithHTTPHeaders(m.ResolvedHeaders()),
 			)
 			if err != nil {
 				slog.Error("error creating mcp client", "error", err)
@@ -218,7 +220,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service, cfg *confi
 		case config.MCPSse:
 			c, err := client.NewSSEMCPClient(
 				m.URL,
-				client.WithHeaders(m.Headers),
+				client.WithHeaders(m.ResolvedHeaders()),
 			)
 			if err != nil {
 				slog.Error("error creating mcp client", "error", err)

internal/tui/components/dialogs/permissions/permissions.go 🔗

@@ -407,13 +407,26 @@ func (p *permissionDialogCmp) generateDefaultContent() string {
 
 	content := p.permission.Description
 
-	// Use the cache for markdown rendering
-	renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
-		r := styles.GetMarkdownRenderer(p.width - 4)
-		s, err := r.Render(content)
-		return s, err
-	})
+	content = strings.TrimSpace(content)
+	content = "\n" + content + "\n"
+	lines := strings.Split(content, "\n")
+
+	width := p.width - 4
+	var out []string
+	for _, ln := range lines {
+		ln = " " + ln // left padding
+		if len(ln) > width {
+			ln = ansi.Truncate(ln, width, "…")
+		}
+		out = append(out, t.S().Muted.
+			Width(width).
+			Foreground(t.FgBase).
+			Background(t.BgSubtle).
+			Render(ln))
+	}
 
+	// Use the cache for markdown rendering
+	renderedContent := strings.Join(out, "\n")
 	finalContent := baseStyle.
 		Width(p.contentViewPort.Width()).
 		Render(renderedContent)