{"record":{"id":"277fb4ec473d9bd2","repo":"charmbracelet/crush","slug":"invalid-mcp-command-w","errorCode":null,"errorMessage":"invalid mcp command: %w","messagePattern":"invalid mcp command: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/agent/tools/mcp/init.go","lineNumber":1040,"sourceCode":"\tif err2 := stdioCheck(ct.Command); err2 != nil {\n\t\terr = errors.Join(err, err2)\n\t}\n\treturn err\n}\n\nfunc maybeTimeoutErr(err error, timeout time.Duration) error {\n\tif errors.Is(err, context.Canceled) {\n\t\treturn fmt.Errorf(\"timed out after %s\", timeout)\n\t}\n\treturn err\n}\n\nfunc createTransport(ctx context.Context, cfg *config.ConfigStore, name string, m config.MCPConfig, resolver config.VariableResolver) (mcp.Transport, *mcpoauth.Handler, error) {\n\tswitch m.Type {\n\tcase config.MCPStdio:\n\t\tcommand, err := resolver.ResolveValue(m.Command)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"invalid mcp command: %w\", err)\n\t\t}\n\t\tif strings.TrimSpace(command) == \"\" {\n\t\t\treturn nil, nil, fmt.Errorf(\"mcp stdio config requires a non-empty 'command' field\")\n\t\t}\n\t\targs, err := m.ResolvedArgs(resolver)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tenvs, err := m.ResolvedEnv(resolver)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tcmd := exec.CommandContext(ctx, home.Long(command), args...)\n\t\tcmd.Env = append(os.Environ(), envs...)\n\t\t// Run the child in its own process group and kill the whole group when\n\t\t// the session context is cancelled. A stdio server often spawns its own\n\t\t// children (signal-mcp launches signal-cli); os/exec's default\n\t\t// cancellation kills only the direct child, orphaning the rest with","sourceCodeStart":1022,"sourceCodeEnd":1058,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/agent/tools/mcp/init.go#L1022-L1058","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the resolver error wrapped in the message for the exact failing variable/command","Ensure every $VAR in the command is set in the environment where crush runs","Verify $(cmd) subcommands exit 0 and the binaries exist on PATH","Simplify to a plain absolute path if dynamic resolution is unnecessary"],"exampleFix":"// before (crushrc)\nmcp myserver command '$(which missing-tool) serve'\n// after\nmcp myserver command '/usr/local/bin/myserver' args 'serve'","handlingStrategy":"validation","validationCode":"cmd := os.ExpandEnv(m.Command)\nif strings.TrimSpace(cmd) == \"\" {\n    return errors.New(\"mcp stdio command resolves to empty; check referenced env vars\")\n}\nif _, err := exec.LookPath(cmd); err != nil {\n    return fmt.Errorf(\"command %q not found on PATH\", cmd)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use absolute paths instead of $(which ...) substitutions","Verify all $VARs in the command are exported in the runtime environment","Test $(cmd) substitutions manually before putting them in config","Prefer plain static commands for MCP stdio servers"],"tags":["mcp","config","variable-resolution"],"backgroundTag":"unresolved-config-variable","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}