From 483303de5b07c10ec53d87fa30254393a78037ca Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Tue, 15 Jul 2025 16:28:24 -0400 Subject: [PATCH 1/4] docs(readme): add MCP info --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index f69a451eaba21f92da001f075ad630fa43ff3aba..7071ed94e9ec656ea06652c421e6f2441962bf58 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,22 @@ Crush can use LSPs for additional context to help inform its decisions, just lik } ``` +### MCPs + +Crush can also use MCPs for additional context. Add LSPs to the config like so: + +``` +{ + "mcp": { + "context7": { + "command": "", + "url": "https://mcp.context7.com/mcp", + "type": "http" + } + }, +} +``` + ### OpenAI-Compatible APIs Crush supports all OpenAI-compatible APIs. Here's an example configuration for Deepseek, which uses an OpenAI-compatible API. Don't forget to set `DEEPSEEK_API_KEY` in your environment. From 6805b3494b733479235c801c8b54ed2f6873ab93 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Wed, 16 Jul 2025 13:55:20 +0200 Subject: [PATCH 2/4] chore: some mcp improvements --- internal/config/config.go | 45 ++++++++++++++++--- internal/llm/agent/mcp-tools.go | 20 +++++---- .../dialogs/permissions/permissions.go | 25 ++++++++--- 3 files changed, 69 insertions(+), 21 deletions(-) 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) From d66797d6da0f49eb36894fe9deeed3a79d75c670 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Wed, 16 Jul 2025 13:56:27 +0200 Subject: [PATCH 3/4] chore: add github mcp --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7071ed94e9ec656ea06652c421e6f2441962bf58..91301808b695c7fd40724b04a764cd7e8c1f587f 100644 --- a/README.md +++ b/README.md @@ -82,15 +82,22 @@ Crush can use LSPs for additional context to help inform its decisions, just lik Crush can also use MCPs for additional context. Add LSPs to the config like so: -``` +```json { "mcp": { "context7": { "command": "", "url": "https://mcp.context7.com/mcp", "type": "http" + }, + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/", + "headers": { + "Authorization": "$(echo Bearer $GH_MCP_TOKEN)" + } } - }, + } } ``` From 972ab6e32af27e4efa3b5951ef0fa9cd7365adc3 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Wed, 16 Jul 2025 14:09:28 +0200 Subject: [PATCH 4/4] chore: remove unnecessary config --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 91301808b695c7fd40724b04a764cd7e8c1f587f..7f428d31ed38ba437d2df4edb6747452d9624e2b 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,6 @@ Crush can also use MCPs for additional context. Add LSPs to the config like so: { "mcp": { "context7": { - "command": "", "url": "https://mcp.context7.com/mcp", "type": "http" },