charmbracelet/crush · error

arg

Error message

arg %d: %w

What it means

MCPConfig.ResolvedArgs resolves each entry of the MCP server's Args list through VariableResolver.ResolveValue, which expands template variables (e.g. {{env.VAR}}) in config values. If resolving any single argument fails, it is wrapped as `arg <index>: <cause>`. Typical causes are references to undefined environment variables or secrets that cannot be found.

Solutions

  1. Read the wrapped cause and the arg index to find the offending argument.
  2. Export the referenced environment variable (e.g. export API_KEY=...) before launching crush, or put it in your shell profile.
  3. Define the value via crush's secrets/env config instead of a bare {{env.X}} reference.
  4. Fix typos in the {{...}} variable name in the MCP args in crushrc.

Example fix

// before (crushrc)
mcp myserver --command npx --args '-y','pkg','--key','{{env.MY_API_KEY}}' // MY_API_KEY unset
// after
export MY_API_KEY=sk-...  # then rerun crush
Defensive patterns

Strategy: validation

Validate before calling

# verify every {{env.X}} referenced in MCP args exists before starting crush
grep -o '{{env\.[A-Z_]*}}' crushrc | sed 's/{{env\.//;s/}}//' | sort -u | while read -r v; do
  [ -n "${!v+x}" ] || echo "missing env var: $v"
done

Try / catch

args, err := m.ResolvedArgs(resolver)
if err != nil {
    var miss *MissingEnvError // example typed cause from the resolver
    if errors.As(err, &miss) {
        return fmt.Errorf("set %s before starting MCP server", miss.Name)
    }
    return err
}

Prevention

When it happens

Trigger: Starting or resolving an MCP server configured in crushrc whose `args` contain an unresolvable template variable — most commonly `{{env.SOME_VAR}}` where SOME_VAR is unset, or an invalid variable reference syntax.

Common situations: Defining an MCP stdio server whose args reference an env var that exists in one shell but not the one crush runs in; CI environments missing a secret; typo in the variable key inside {{...}}.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/9d72695520a39b58. Report an issue: GitHub.

Appendix: source

Thrown at internal/config/config.go:458

}

// ResolvedArgs returns m.Args with every element expanded through the
// given resolver. A fresh slice is allocated; m.Args is never mutated.
// On the first resolution failure it returns nil and an error
// identifying the offending positional index; the inner resolver error
// is already sanitized by ResolveValue and is wrapped with %w so
// errors.Is/As continues to work.
//
// See ResolvedEnv for guidance on picking a resolver.
func (m MCPConfig) ResolvedArgs(r VariableResolver) ([]string, error) {
	if len(m.Args) == 0 {
		return nil, nil
	}
	out := make([]string, len(m.Args))
	for i, a := range m.Args {
		v, err := r.ResolveValue(a)
		if err != nil {
			return nil, fmt.Errorf("arg %d: %w", i, err)
		}
		out[i] = v
	}
	return out, nil
}

// ResolvedURL returns m.URL expanded through the given resolver. The
// receiver is not mutated. Errors from the resolver are already
// sanitized by ResolveValue and are wrapped with %w for errors.Is/As.
//
// URLs run through the same shell-expansion pipeline as the other
// fields, so a literal '$' (e.g. OData query strings containing
// $filter/$select) must be escaped as '\$' or '${DOLLAR:-$}' to avoid
// being interpreted as a variable reference. Same constraint already
// applies to command, args, env, and headers.
//
// See ResolvedEnv for guidance on picking a resolver.
func (m MCPConfig) ResolvedURL(r VariableResolver) (string, error) {

View on GitHub (pinned to 7944b8e522)