@@ -14,7 +14,6 @@ import (
15 "charm.land/catwalk/pkg/catwalk"
15 "charm.land/catwalk/pkg/catwalk"
16 "github.com/charmbracelet/crush/internal/csync"
16 "github.com/charmbracelet/crush/internal/csync"
17− "github.com/charmbracelet/crush/internal/env"
18 "github.com/charmbracelet/crush/internal/oauth"
17 "github.com/charmbracelet/crush/internal/oauth"
19 "github.com/charmbracelet/crush/internal/oauth/copilot"
18 "github.com/charmbracelet/crush/internal/oauth/copilot"
20 "github.com/invopop/jsonschema"
19 "github.com/invopop/jsonschema"
@@ -301,7 +300,7 @@ func (l LSPs) Sorted() []LSP {
303// ResolvedEnv returns m.Env with every value expanded through the
302// 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
305// by key so callers get deterministic output; the receiver's Env map is
304// by key so callers get deterministic output; the receiver's Env map is
306// not mutated. On the first resolution failure it returns nil and an
305// not mutated. On the first resolution failure it returns nil and an
307// error that identifies the offending key; the inner resolver error is
306// error that identifies the offending key; the inner resolver error is
@@ -309,21 +308,49 @@ func (l LSPs) Sorted() []LSP {
309// errors.Is/As continues to work. Callers are expected to surface it
308// errors.Is/As continues to work. Callers are expected to surface it
310// (for MCP, via StateError on the status card) rather than silently
309// (for MCP, via StateError on the status card) rather than silently
311// spawn the server with an empty credential.
310// spawn the server with an empty credential.
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.
312−func (m MCPConfig) ResolvedEnv() ([]string, error) {
315+func (m MCPConfig) ResolvedEnv(r VariableResolver) ([]string, error) {
313− return resolveEnvs(m.Env)
316+ return resolveEnvs(m.Env, r)
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.
326+// See ResolvedEnv for guidance on picking a resolver.
327+func (m MCPConfig) ResolvedArgs(r VariableResolver) ([]string, error) {
328+ if len(m.Args) == 0 {
331+ out := make([]string, len(m.Args))
332+ for i, a := range m.Args {
333+ v, err := r.ResolveValue(a)
335+ return nil, fmt.Errorf("arg %d: %w", i, err)
316// ResolvedHeaders returns m.Headers with every value expanded through
342// 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
318// mutated. On the first resolution failure it returns nil and an error
344// mutated. On the first resolution failure it returns nil and an error
319// identifying the offending header name; the inner resolver error is
345// identifying the offending header name; the inner resolver error is
320// already sanitized by ResolveValue and is wrapped with %w so
346// already sanitized by ResolveValue and is wrapped with %w so
321// errors.Is/As continues to work.
347// errors.Is/As continues to work.
349+// See ResolvedEnv for guidance on picking a resolver.
322−func (m MCPConfig) ResolvedHeaders() (map[string]string, error) {
350+func (m MCPConfig) ResolvedHeaders(r VariableResolver) (map[string]string, error) {
323 if len(m.Headers) == 0 {
351 if len(m.Headers) == 0 {
324 return map[string]string{}, nil
352 return map[string]string{}, nil
326− resolver := NewShellVariableResolver(env.New())
327 out := make(map[string]string, len(m.Headers))
354 out := make(map[string]string, len(m.Headers))
328 // Sort keys so failures are reported deterministically when more
355 // Sort keys so failures are reported deterministically when more
329 // than one header would fail.
356 // than one header would fail.
@@ -333,7 +360,7 @@ func (m MCPConfig) ResolvedHeaders() (map[string]string, error) {
334 slices.Sort(keys)
361 slices.Sort(keys)
335 for _, k := range keys {
362 for _, k := range keys {
336− v, err := resolver.ResolveValue(m.Headers[k])
363+ v, err := r.ResolveValue(m.Headers[k])
337 if err != nil {
364 if err != nil {
338 return nil, fmt.Errorf("header %s: %w", k, err)
365 return nil, fmt.Errorf("header %s: %w", k, err)
@@ -679,16 +706,15 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
679 return nil
706 return nil
682−// resolveEnvs expands every value in envs through the shell resolver
709+// resolveEnvs expands every value in envs through the given resolver
683// and returns a fresh "KEY=value" slice sorted by key. The input map is
710// and returns a fresh "KEY=value" slice sorted by key. The input map is
684// not mutated. On the first resolution failure it returns nil and an
711// not mutated. On the first resolution failure it returns nil and an
685// error identifying the offending variable; the inner resolver error is
712// error identifying the offending variable; the inner resolver error is
686// already sanitized by ResolveValue and is wrapped with %w.
713// 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) {
688 if len(envs) == 0 {
715 if len(envs) == 0 {
689 return nil, nil
716 return nil, nil
691− resolver := NewShellVariableResolver(env.New())
692 keys := make([]string, 0, len(envs))
718 keys := make([]string, 0, len(envs))
693 for k := range envs {
719 for k := range envs {
694 keys = append(keys, k)
720 keys = append(keys, k)
@@ -696,7 +722,7 @@ func resolveEnvs(envs map[string]string) ([]string, error) {
696 slices.Sort(keys)
722 slices.Sort(keys)
697 res := make([]string, 0, len(envs))
723 res := make([]string, 0, len(envs))
698 for _, k := range keys {
724 for _, k := range keys {
699− v, err := resolver.ResolveValue(envs[k])
725+ v, err := r.ResolveValue(envs[k])
700 if err != nil {
726 if err != nil {
701 return nil, fmt.Errorf("env %s: %w", k, err)
727 return nil, fmt.Errorf("env %s: %w", k, err)