@@ -14,7 +14,6 @@ import (
1414
1515 "charm.land/catwalk/pkg/catwalk"
1616 "github.com/charmbracelet/crush/internal/csync"
17− "github.com/charmbracelet/crush/internal/env"
1817 "github.com/charmbracelet/crush/internal/oauth"
1918 "github.com/charmbracelet/crush/internal/oauth/copilot"
2019 "github.com/invopop/jsonschema"
@@ -301,7 +300,7 @@ func (l LSPs) Sorted() []LSP {
301300}
302301
303302// ResolvedEnv returns m.Env with every value expanded through the
304−// shell resolver. The returned slice is of the form "KEY=value" sorted
303+// given resolver. The returned slice is of the form "KEY=value" sorted
305304// by key so callers get deterministic output; the receiver's Env map is
306305// not mutated. On the first resolution failure it returns nil and an
307306// error that identifies the offending key; the inner resolver error is
@@ -309,21 +308,49 @@ func (l LSPs) Sorted() []LSP {
309308// errors.Is/As continues to work. Callers are expected to surface it
310309// (for MCP, via StateError on the status card) rather than silently
311310// spawn the server with an empty credential.
312−func (m MCPConfig) ResolvedEnv() ([]string, error) {
313− return resolveEnvs(m.Env)
311+//
312+// The resolver choice matters: in server mode pass the shell resolver
313+// so $VAR / $(cmd) expand; in client mode pass IdentityResolver so the
314+// template is forwarded verbatim and expansion happens on the server.
315+func (m MCPConfig) ResolvedEnv(r VariableResolver) ([]string, error) {
316+ return resolveEnvs(m.Env, r)
317+}
318+
319+// ResolvedArgs returns m.Args with every element expanded through the
320+// given resolver. A fresh slice is allocated; m.Args is never mutated.
321+// On the first resolution failure it returns nil and an error
322+// identifying the offending positional index; the inner resolver error
323+// is already sanitized by ResolveValue and is wrapped with %w so
324+// errors.Is/As continues to work.
325+//
326+// See ResolvedEnv for guidance on picking a resolver.
327+func (m MCPConfig) ResolvedArgs(r VariableResolver) ([]string, error) {
328+ if len(m.Args) == 0 {
329+ return nil, nil
330+ }
331+ out := make([]string, len(m.Args))
332+ for i, a := range m.Args {
333+ v, err := r.ResolveValue(a)
334+ if err != nil {
335+ return nil, fmt.Errorf("arg %d: %w", i, err)
336+ }
337+ out[i] = v
338+ }
339+ return out, nil
314340}
315341
316342// ResolvedHeaders returns m.Headers with every value expanded through
317−// the shell resolver. A fresh map is allocated; m.Headers is never
343+// the given resolver. A fresh map is allocated; m.Headers is never
318344// mutated. On the first resolution failure it returns nil and an error
319345// identifying the offending header name; the inner resolver error is
320346// already sanitized by ResolveValue and is wrapped with %w so
321347// errors.Is/As continues to work.
322−func (m MCPConfig) ResolvedHeaders() (map[string]string, error) {
348+//
349+// See ResolvedEnv for guidance on picking a resolver.
350+func (m MCPConfig) ResolvedHeaders(r VariableResolver) (map[string]string, error) {
323351 if len(m.Headers) == 0 {
324352 return map[string]string{}, nil
325353 }
326− resolver := NewShellVariableResolver(env.New())
327354 out := make(map[string]string, len(m.Headers))
328355 // Sort keys so failures are reported deterministically when more
329356 // than one header would fail.
@@ -333,7 +360,7 @@ func (m MCPConfig) ResolvedHeaders() (map[string]string, error) {
333360 }
334361 slices.Sort(keys)
335362 for _, k := range keys {
336− v, err := resolver.ResolveValue(m.Headers[k])
363+ v, err := r.ResolveValue(m.Headers[k])
337364 if err != nil {
338365 return nil, fmt.Errorf("header %s: %w", k, err)
339366 }
@@ -679,16 +706,15 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
679706 return nil
680707}
681708
682−// resolveEnvs expands every value in envs through the shell resolver
709+// resolveEnvs expands every value in envs through the given resolver
683710// and returns a fresh "KEY=value" slice sorted by key. The input map is
684711// not mutated. On the first resolution failure it returns nil and an
685712// error identifying the offending variable; the inner resolver error is
686713// already sanitized by ResolveValue and is wrapped with %w.
687−func resolveEnvs(envs map[string]string) ([]string, error) {
714+func resolveEnvs(envs map[string]string, r VariableResolver) ([]string, error) {
688715 if len(envs) == 0 {
689716 return nil, nil
690717 }
691− resolver := NewShellVariableResolver(env.New())
692718 keys := make([]string, 0, len(envs))
693719 for k := range envs {
694720 keys = append(keys, k)
@@ -696,7 +722,7 @@ func resolveEnvs(envs map[string]string) ([]string, error) {
696722 slices.Sort(keys)
697723 res := make([]string, 0, len(envs))
698724 for _, k := range keys {
699− v, err := resolver.ResolveValue(envs[k])
725+ v, err := r.ResolveValue(envs[k])
700726 if err != nil {
701727 return nil, fmt.Errorf("env %s: %w", k, err)
702728 }