on main help sign in
fix(mcp/lsp): expand variable in commands (#1116)

closes #806 Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Co-authored-by: taigr <taigr@users.noreply.github.com>

commit09d8e75b7be61fb501ecd28682b5a56717db4465 authorCarlos Alexandro Becker <caarlos0@users.noreply.github.com> dateWed 24 Sep 2025, 14:35 (1 year ago) parent2cc3fe2
4 files changed+27−14
internal/app/lsp.go+1−1 internal/llm/agent/mcp-tools.go+13−8 internal/lsp/client.go+7−2 internal/lsp/client_test.go+6−3
internal/app/lsp.go+1−1
@@ -36,7 +36,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config
3636 updateLSPState(name, lsp.StateStarting, nil, nil, 0)
3737
3838 // Create LSP client.
39− lspClient, err := lsp.New(ctx, name, config)
39+ lspClient, err := lsp.New(ctx, name, config, app.config.Resolver())
4040 if err != nil {
4141 slog.Error("Failed to create LSP client for", name, err)
4242 updateLSPState(name, lsp.StateError, err, nil, 0)
internal/llm/agent/mcp-tools.go+13−8
@@ -149,7 +149,8 @@ func getOrRenewClient(ctx context.Context, name string) (*client.Client, error)
149149 return nil, fmt.Errorf("mcp '%s' not available", name)
150150 }
151151
152− m := config.Get().MCP[name]
152+ cfg := config.Get()
153+ m := cfg.MCP[name]
153154 state, _ := mcpStates.Get(name)
154155
155156 timeout := mcpTimeout(m)
@@ -161,7 +162,7 @@ func getOrRenewClient(ctx context.Context, name string) (*client.Client, error)
161162 }
162163 updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, state.ToolCount)
163164
164− c, err = createAndInitializeClient(ctx, name, m)
165+ c, err = createAndInitializeClient(ctx, name, m, cfg.Resolver())
165166 if err != nil {
166167 return nil, err
167168 }
@@ -313,7 +314,7 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
313314
314315 ctx, cancel := context.WithTimeout(ctx, mcpTimeout(m))
315316 defer cancel()
316− c, err := createAndInitializeClient(ctx, name, m)
317+ c, err := createAndInitializeClient(ctx, name, m, cfg.Resolver())
317318 if err != nil {
318319 return
319320 }
@@ -328,8 +329,8 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
328329 return slices.Collect(result.Seq())
329330}
330331
331−func createAndInitializeClient(ctx context.Context, name string, m config.MCPConfig) (*client.Client, error) {
332− c, err := createMcpClient(name, m)
332+func createAndInitializeClient(ctx context.Context, name string, m config.MCPConfig, resolver config.VariableResolver) (*client.Client, error) {
333+ c, err := createMcpClient(name, m, resolver)
333334 if err != nil {
334335 updateMCPState(name, MCPStateError, err, nil, 0)
335336 slog.Error("error creating mcp client", "error", err, "name", name)
@@ -367,14 +368,18 @@ func maybeTimeoutErr(err error, timeout time.Duration) error {
367368 return err
368369}
369370
370−func createMcpClient(name string, m config.MCPConfig) (*client.Client, error) {
371+func createMcpClient(name string, m config.MCPConfig, resolver config.VariableResolver) (*client.Client, error) {
371372 switch m.Type {
372373 case config.MCPStdio:
373− if strings.TrimSpace(m.Command) == "" {
374+ command, err := resolver.ResolveValue(m.Command)
375+ if err != nil {
376+ return nil, fmt.Errorf("invalid mcp command: %w", err)
377+ }
378+ if strings.TrimSpace(command) == "" {
374379 return nil, fmt.Errorf("mcp stdio config requires a non-empty 'command' field")
375380 }
376381 return client.NewStdioMCPClientWithOptions(
377− home.Long(m.Command),
382+ home.Long(command),
378383 m.ResolvedEnv(),
379384 m.Args,
380385 transport.WithCommandLogger(mcpLogger{name: name}),
internal/lsp/client.go+7−2
@@ -45,7 +45,7 @@ type Client struct {
4545}
4646
4747// New creates a new LSP client using the powernap implementation.
48−func New(ctx context.Context, name string, config config.LSPConfig) (*Client, error) {
48+func New(ctx context.Context, name string, config config.LSPConfig, resolver config.VariableResolver) (*Client, error) {
4949 // Convert working directory to file URI
5050 workDir, err := os.Getwd()
5151 if err != nil {
@@ -54,9 +54,14 @@ func New(ctx context.Context, name string, config config.LSPConfig) (*Client, er
5454
5555 rootURI := string(protocol.URIFromPath(workDir))
5656
57+ command, err := resolver.ResolveValue(config.Command)
58+ if err != nil {
59+ return nil, fmt.Errorf("invalid lsp command: %w", err)
60+ }
61+
5762 // Create powernap client config
5863 clientConfig := powernap.ClientConfig{
59− Command: home.Long(config.Command),
64+ Command: home.Long(command),
6065 Args: config.Args,
6166 RootURI: rootURI,
6267 Environment: func() map[string]string {
internal/lsp/client_test.go+6−3
@@ -5,14 +5,15 @@ import (
55 "testing"
66
77 "github.com/charmbracelet/crush/internal/config"
8+ "github.com/charmbracelet/crush/internal/env"
89)
910
10−func TestPowernapClient(t *testing.T) {
11+func TestClient(t *testing.T) {
1112 ctx := context.Background()
1213
1314 // Create a simple config for testing
1415 cfg := config.LSPConfig{
15− Command: "echo", // Use echo as a dummy command that won't fail
16+ Command: "$THE_CMD", // Use echo as a dummy command that won't fail
1617 Args: []string{"hello"},
1718 FileTypes: []string{"go"},
1819 Env: map[string]string{},
@@ -20,7 +21,9 @@ func TestPowernapClient(t *testing.T) {
2021
2122 // Test creating a powernap client - this will likely fail with echo
2223 // but we can still test the basic structure
23− client, err := New(ctx, "test", cfg)
24+ client, err := New(ctx, "test", cfg, config.NewEnvironmentVariableResolver(env.NewFromMap(map[string]string{
25+ "THE_CMD": "echo",
26+ })))
2427 if err != nil {
2528 // Expected to fail with echo command, skip the rest
2629 t.Skipf("Powernap client creation failed as expected with dummy command: %v", err)