diff --git a/internal/config/config.go b/internal/config/config.go index 354005ac42f8fb1d179442353b819e871cc61259..d63a34f73d5210c2542be8a598ef38cb06339bd9 100644 --- a/internal/config/config.go +++ b/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"` diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go index d8610b557896272d94c76c608b9bc00347655be4..c655e01815c45959247ba0f02241232346dc166f 100644 --- a/internal/llm/agent/mcp-tools.go +++ b/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) diff --git a/internal/tui/components/dialogs/permissions/permissions.go b/internal/tui/components/dialogs/permissions/permissions.go index 0bbaa034ed2357cc4643ad92c0a680bb01cf61ff..fa08885e7db516f11248430e74046e978dd00e88 100644 --- a/internal/tui/components/dialogs/permissions/permissions.go +++ b/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)