on main help sign in
Merge pull request #209 from charmbracelet/mcp-readme
62001ddKujtim Hoxha
README.md+22
@@ -78,6 +78,28 @@ Crush can use LSPs for additional context to help inform its decisions, just lik
78}
78}
79```
79```
80
80
81+### MCPs
82+
83+Crush can also use MCPs for additional context. Add LSPs to the config like so:
84+
85+```json
86+{
87+ "mcp": {
88+ "context7": {
89+ "url": "https://mcp.context7.com/mcp",
90+ "type": "http"
91+ },
92+ "github": {
93+ "type": "http",
94+ "url": "https://api.githubcopilot.com/mcp/",
95+ "headers": {
96+ "Authorization": "$(echo Bearer $GH_MCP_TOKEN)"
97+ }
98+ }
99+ }
100+}
101+```
102+
81### OpenAI-Compatible APIs
103### OpenAI-Compatible APIs
82
104
83Crush 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.
105Crush 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.
internal/config/config.go+39−6
@@ -6,8 +6,10 @@ import (
6 "slices"
6 "slices"
7 "strings"
7 "strings"
8
8
9+ "github.com/charmbracelet/crush/internal/env"
9 "github.com/charmbracelet/crush/internal/fur/provider"
10 "github.com/charmbracelet/crush/internal/fur/provider"
10 "github.com/tidwall/sjson"
11 "github.com/tidwall/sjson"
12+ "golang.org/x/exp/slog"
11)
13)
12
14
13const (
15const (
@@ -90,12 +92,12 @@ const (
90)
92)
91
93
92type MCPConfig struct {
94type MCPConfig struct {
93− Command string `json:"command,omitempty" `
95+ Command string `json:"command,omitempty" `
94− Env []string `json:"env,omitempty"`
96+ Env map[string]string `json:"env,omitempty"`
95− Args []string `json:"args,omitempty"`
97+ Args []string `json:"args,omitempty"`
96− Type MCPType `json:"type"`
98+ Type MCPType `json:"type"`
97− URL string `json:"url,omitempty"`
99+ URL string `json:"url,omitempty"`
98− Disabled bool `json:"disabled,omitempty"`
100+ Disabled bool `json:"disabled,omitempty"`
99
101
100 // TODO: maybe make it possible to get the value from the env
102 // TODO: maybe make it possible to get the value from the env
101 Headers map[string]string `json:"headers,omitempty"`
103 Headers map[string]string `json:"headers,omitempty"`
@@ -165,6 +167,37 @@ func (l LSPs) Sorted() []LSP {
165 return sorted
167 return sorted
166}
168}
167
169
170+func (m MCPConfig) ResolvedEnv() []string {
171+ resolver := NewShellVariableResolver(env.New())
172+ for e, v := range m.Env {
173+ var err error
174+ m.Env[e], err = resolver.ResolveValue(v)
175+ if err != nil {
176+ slog.Error("error resolving environment variable", "error", err, "variable", e, "value", v)
177+ continue
178+ }
179+ }
180+
181+ env := make([]string, 0, len(m.Env))
182+ for k, v := range m.Env {
183+ env = append(env, fmt.Sprintf("%s=%s", k, v))
184+ }
185+ return env
186+}
187+
188+func (m MCPConfig) ResolvedHeaders() map[string]string {
189+ resolver := NewShellVariableResolver(env.New())
190+ for e, v := range m.Headers {
191+ var err error
192+ m.Headers[e], err = resolver.ResolveValue(v)
193+ if err != nil {
194+ slog.Error("error resolving header variable", "error", err, "variable", e, "value", v)
195+ continue
196+ }
197+ }
198+ return m.Headers
199+}
200+
168type Agent struct {
201type Agent struct {
169 ID string `json:"id,omitempty"`
202 ID string `json:"id,omitempty"`
170 Name string `json:"name,omitempty"`
203 Name string `json:"name,omitempty"`
internal/llm/agent/mcp-tools.go+9−9
@@ -37,7 +37,7 @@ type MCPClient interface {
37}
37}
38
38
39func (b *mcpTool) Name() string {
39func (b *mcpTool) Name() string {
40− return fmt.Sprintf("%s_%s", b.mcpName, b.tool.Name)
40+ return fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name)
41}
41}
42
42
43func (b *mcpTool) Info() tools.ToolInfo {
43func (b *mcpTool) Info() tools.ToolInfo {
@@ -46,7 +46,7 @@ func (b *mcpTool) Info() tools.ToolInfo {
46 required = make([]string, 0)
46 required = make([]string, 0)
47 }
47 }
48 return tools.ToolInfo{
48 return tools.ToolInfo{
49− Name: fmt.Sprintf("%s_%s", b.mcpName, b.tool.Name),
49+ Name: fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name),
50 Description: b.tool.Description,
50 Description: b.tool.Description,
51 Parameters: b.tool.InputSchema.Properties,
51 Parameters: b.tool.InputSchema.Properties,
52 Required: required,
52 Required: required,
@@ -108,14 +108,14 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
108 },
108 },
109 )
109 )
110 if !p {
110 if !p {
111− return tools.NewTextErrorResponse("permission denied"), nil
111+ return tools.ToolResponse{}, permission.ErrorPermissionDenied
112 }
112 }
113
113
114 switch b.mcpConfig.Type {
114 switch b.mcpConfig.Type {
115 case config.MCPStdio:
115 case config.MCPStdio:
116 c, err := client.NewStdioMCPClient(
116 c, err := client.NewStdioMCPClient(
117 b.mcpConfig.Command,
117 b.mcpConfig.Command,
118− b.mcpConfig.Env,
118+ b.mcpConfig.ResolvedEnv(),
119 b.mcpConfig.Args...,
119 b.mcpConfig.Args...,
120 )
120 )
121 if err != nil {
121 if err != nil {
@@ -125,7 +125,7 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
125 case config.MCPHttp:
125 case config.MCPHttp:
126 c, err := client.NewStreamableHttpClient(
126 c, err := client.NewStreamableHttpClient(
127 b.mcpConfig.URL,
127 b.mcpConfig.URL,
128− transport.WithHTTPHeaders(b.mcpConfig.Headers),
128+ transport.WithHTTPHeaders(b.mcpConfig.ResolvedHeaders()),
129 )
129 )
130 if err != nil {
130 if err != nil {
131 return tools.NewTextErrorResponse(err.Error()), nil
131 return tools.NewTextErrorResponse(err.Error()), nil
@@ -134,7 +134,7 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
134 case config.MCPSse:
134 case config.MCPSse:
135 c, err := client.NewSSEMCPClient(
135 c, err := client.NewSSEMCPClient(
136 b.mcpConfig.URL,
136 b.mcpConfig.URL,
137− client.WithHeaders(b.mcpConfig.Headers),
137+ client.WithHeaders(b.mcpConfig.ResolvedHeaders()),
138 )
138 )
139 if err != nil {
139 if err != nil {
140 return tools.NewTextErrorResponse(err.Error()), nil
140 return tools.NewTextErrorResponse(err.Error()), nil
@@ -210,7 +210,7 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
210 case config.MCPStdio:
210 case config.MCPStdio:
211 c, err := client.NewStdioMCPClient(
211 c, err := client.NewStdioMCPClient(
212 m.Command,
212 m.Command,
213− m.Env,
213+ m.ResolvedEnv(),
214 m.Args...,
214 m.Args...,
215 )
215 )
216 if err != nil {
216 if err != nil {
@@ -224,7 +224,7 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
224 case config.MCPHttp:
224 case config.MCPHttp:
225 c, err := client.NewStreamableHttpClient(
225 c, err := client.NewStreamableHttpClient(
226 m.URL,
226 m.URL,
227− transport.WithHTTPHeaders(m.Headers),
227+ transport.WithHTTPHeaders(m.ResolvedHeaders()),
228 )
228 )
229 if err != nil {
229 if err != nil {
230 slog.Error("error creating mcp client", "error", err)
230 slog.Error("error creating mcp client", "error", err)
@@ -236,7 +236,7 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
236 case config.MCPSse:
236 case config.MCPSse:
237 c, err := client.NewSSEMCPClient(
237 c, err := client.NewSSEMCPClient(
238 m.URL,
238 m.URL,
239− client.WithHeaders(m.Headers),
239+ client.WithHeaders(m.ResolvedHeaders()),
240 )
240 )
241 if err != nil {
241 if err != nil {
242 slog.Error("error creating mcp client", "error", err)
242 slog.Error("error creating mcp client", "error", err)
internal/tui/components/dialogs/permissions/permissions.go+19−6
@@ -409,13 +409,26 @@ func (p *permissionDialogCmp) generateDefaultContent() string {
409
409
410 content := p.permission.Description
410 content := p.permission.Description
411
411
412− // Use the cache for markdown rendering
412+ content = strings.TrimSpace(content)
413− renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
413+ content = "\n" + content + "\n"
414− r := styles.GetMarkdownRenderer(p.width - 4)
414+ lines := strings.Split(content, "\n")
415+
416+ width := p.width - 4
417+ var out []string
418+ for _, ln := range lines {
419+ ln = " " + ln // left padding
420+ if len(ln) > width {
421+ ln = ansi.Truncate(ln, width, "…")
422+ }
423+ out = append(out, t.S().Muted.
424+ Width(width).
425+ Foreground(t.FgBase).
426+ Background(t.BgSubtle).
415− s, err := r.Render(content)
427+ Render(ln))
416− return s, err
417− })
428+ }
418
429
430+ // Use the cache for markdown rendering
431+ renderedContent := strings.Join(out, "\n")
419 finalContent := baseStyle.
432 finalContent := baseStyle.
420 Width(p.contentViewPort.Width()).
433 Width(p.contentViewPort.Width()).
421 Render(renderedContent)
434 Render(renderedContent)