on main help sign in
Merge pull request #209 from charmbracelet/mcp-readme

docs(readme): add MCP info

commit62001ddfdaae1ce1779e5a49e559c6d378e47ce8 authorKujtim Hoxha <kujtimii.h@gmail.com> dateWed 16 Jul 2025, 22:41 (1 year ago) parent08c5b4c972ab6e
4 files changed+89−21
README.md+22 internal/config/config.go+39−6 internal/llm/agent/mcp-tools.go+9−9 internal/tui/components/dialogs/permissions/permissions.go+19−6
README.md+22
@@ -78,6 +78,28 @@ Crush can use LSPs for additional context to help inform its decisions, just lik
7878}
7979```
8080
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+
81103### OpenAI-Compatible APIs
82104
83105Crush 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 (
66 "slices"
77 "strings"
88
9+ "github.com/charmbracelet/crush/internal/env"
910 "github.com/charmbracelet/crush/internal/fur/provider"
1011 "github.com/tidwall/sjson"
12+ "golang.org/x/exp/slog"
1113)
1214
1315const (
@@ -90,12 +92,12 @@ const (
9092)
9193
9294type MCPConfig struct {
93− Command string `json:"command,omitempty" `
94− Env []string `json:"env,omitempty"`
95− Args []string `json:"args,omitempty"`
96− Type MCPType `json:"type"`
97− URL string `json:"url,omitempty"`
98− Disabled bool `json:"disabled,omitempty"`
95+ Command string `json:"command,omitempty" `
96+ Env map[string]string `json:"env,omitempty"`
97+ Args []string `json:"args,omitempty"`
98+ Type MCPType `json:"type"`
99+ URL string `json:"url,omitempty"`
100+ Disabled bool `json:"disabled,omitempty"`
99101
100102 // TODO: maybe make it possible to get the value from the env
101103 Headers map[string]string `json:"headers,omitempty"`
@@ -165,6 +167,37 @@ func (l LSPs) Sorted() []LSP {
165167 return sorted
166168}
167169
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+
168201type Agent struct {
169202 ID string `json:"id,omitempty"`
170203 Name string `json:"name,omitempty"`
internal/llm/agent/mcp-tools.go+9−9
@@ -37,7 +37,7 @@ type MCPClient interface {
3737}
3838
3939func (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)
4141}
4242
4343func (b *mcpTool) Info() tools.ToolInfo {
@@ -46,7 +46,7 @@ func (b *mcpTool) Info() tools.ToolInfo {
4646 required = make([]string, 0)
4747 }
4848 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),
5050 Description: b.tool.Description,
5151 Parameters: b.tool.InputSchema.Properties,
5252 Required: required,
@@ -108,14 +108,14 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
108108 },
109109 )
110110 if !p {
111− return tools.NewTextErrorResponse("permission denied"), nil
111+ return tools.ToolResponse{}, permission.ErrorPermissionDenied
112112 }
113113
114114 switch b.mcpConfig.Type {
115115 case config.MCPStdio:
116116 c, err := client.NewStdioMCPClient(
117117 b.mcpConfig.Command,
118− b.mcpConfig.Env,
118+ b.mcpConfig.ResolvedEnv(),
119119 b.mcpConfig.Args...,
120120 )
121121 if err != nil {
@@ -125,7 +125,7 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
125125 case config.MCPHttp:
126126 c, err := client.NewStreamableHttpClient(
127127 b.mcpConfig.URL,
128− transport.WithHTTPHeaders(b.mcpConfig.Headers),
128+ transport.WithHTTPHeaders(b.mcpConfig.ResolvedHeaders()),
129129 )
130130 if err != nil {
131131 return tools.NewTextErrorResponse(err.Error()), nil
@@ -134,7 +134,7 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
134134 case config.MCPSse:
135135 c, err := client.NewSSEMCPClient(
136136 b.mcpConfig.URL,
137− client.WithHeaders(b.mcpConfig.Headers),
137+ client.WithHeaders(b.mcpConfig.ResolvedHeaders()),
138138 )
139139 if err != nil {
140140 return tools.NewTextErrorResponse(err.Error()), nil
@@ -210,7 +210,7 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
210210 case config.MCPStdio:
211211 c, err := client.NewStdioMCPClient(
212212 m.Command,
213− m.Env,
213+ m.ResolvedEnv(),
214214 m.Args...,
215215 )
216216 if err != nil {
@@ -224,7 +224,7 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
224224 case config.MCPHttp:
225225 c, err := client.NewStreamableHttpClient(
226226 m.URL,
227− transport.WithHTTPHeaders(m.Headers),
227+ transport.WithHTTPHeaders(m.ResolvedHeaders()),
228228 )
229229 if err != nil {
230230 slog.Error("error creating mcp client", "error", err)
@@ -236,7 +236,7 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
236236 case config.MCPSse:
237237 c, err := client.NewSSEMCPClient(
238238 m.URL,
239− client.WithHeaders(m.Headers),
239+ client.WithHeaders(m.ResolvedHeaders()),
240240 )
241241 if err != nil {
242242 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 {
409409
410410 content := p.permission.Description
411411
412− // Use the cache for markdown rendering
413− renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
414− r := styles.GetMarkdownRenderer(p.width - 4)
415− s, err := r.Render(content)
416− return s, err
417− })
412+ content = strings.TrimSpace(content)
413+ content = "\n" + content + "\n"
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).
427+ Render(ln))
428+ }
418429
430+ // Use the cache for markdown rendering
431+ renderedContent := strings.Join(out, "\n")
419432 finalContent := baseStyle.
420433 Width(p.contentViewPort.Width()).
421434 Render(renderedContent)