charmbracelet/crush · error

invalid mcp command: %w

Error message

invalid mcp command: %w

What it means

createTransport resolves the MCP stdio 'command' field through the variable resolver (supporting $VAR and $(cmd) substitution). If the resolver fails, the error is wrapped as 'invalid mcp command'. This happens before any process is spawned.

Source

Thrown at internal/agent/tools/mcp/init.go:1040

	if err2 := stdioCheck(ct.Command); err2 != nil {
		err = errors.Join(err, err2)
	}
	return err
}

func maybeTimeoutErr(err error, timeout time.Duration) error {
	if errors.Is(err, context.Canceled) {
		return fmt.Errorf("timed out after %s", timeout)
	}
	return err
}

func createTransport(ctx context.Context, cfg *config.ConfigStore, name string, m config.MCPConfig, resolver config.VariableResolver) (mcp.Transport, *mcpoauth.Handler, error) {
	switch m.Type {
	case config.MCPStdio:
		command, err := resolver.ResolveValue(m.Command)
		if err != nil {
			return nil, nil, fmt.Errorf("invalid mcp command: %w", err)
		}
		if strings.TrimSpace(command) == "" {
			return nil, nil, fmt.Errorf("mcp stdio config requires a non-empty 'command' field")
		}
		args, err := m.ResolvedArgs(resolver)
		if err != nil {
			return nil, nil, err
		}
		envs, err := m.ResolvedEnv(resolver)
		if err != nil {
			return nil, nil, err
		}
		cmd := exec.CommandContext(ctx, home.Long(command), args...)
		cmd.Env = append(os.Environ(), envs...)
		// Run the child in its own process group and kill the whole group when
		// the session context is cancelled. A stdio server often spawns its own
		// children (signal-mcp launches signal-cli); os/exec's default
		// cancellation kills only the direct child, orphaning the rest with

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Check the resolver error wrapped in the message for the exact failing variable/command
  2. Ensure every $VAR in the command is set in the environment where crush runs
  3. Verify $(cmd) subcommands exit 0 and the binaries exist on PATH
  4. Simplify to a plain absolute path if dynamic resolution is unnecessary

Example fix

// before (crushrc)
mcp myserver command '$(which missing-tool) serve'
// after
mcp myserver command '/usr/local/bin/myserver' args 'serve'
Defensive patterns

Strategy: validation

Validate before calling

cmd := os.ExpandEnv(m.Command)
if strings.TrimSpace(cmd) == "" {
    return errors.New("mcp stdio command resolves to empty; check referenced env vars")
}
if _, err := exec.LookPath(cmd); err != nil {
    return fmt.Errorf("command %q not found on PATH", cmd)
}

Prevention

When it happens

Trigger: The command string in an MCP stdio config contains an unresolvable $VAR, a failing $(cmd) substitution, or the resolver otherwise errors on the command value.

Common situations: Referencing an env var that is not set (e.g. $HOME-based paths in service contexts); command using $(which tool) where the tool is not installed; quoting mistakes in the config.

Related errors


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