@@ -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"`
@@ -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)
@@ -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)