{"record":{"id":"2b5eb098149d5d8a","repo":"charmbracelet/crush","slug":"oauth-client-id-w","errorCode":null,"errorMessage":"oauth_client_id: %w","messagePattern":"oauth_client_id: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/agent/tools/mcp/init.go","lineNumber":1093,"sourceCode":"\t\t// on every exchange and refresh via this saver.\n\t\tif m.OAuth {\n\t\t\ttokenSaver := func(tok *oauth.Token) {\n\t\t\t\tif err := cfg.SetConfigField(config.ScopeGlobal, fmt.Sprintf(\"mcp.%s.oauth_token\", name), tok); err != nil {\n\t\t\t\t\tslog.Warn(\"Failed to persist MCP OAuth token\", \"name\", name, \"error\", err)\n\t\t\t\t} else {\n\t\t\t\t\tslog.Info(\"Persisted MCP OAuth token\", \"name\", name)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// A pre-registered client is required for servers that do not\n\t\t\t// support dynamic client registration (e.g. GitHub, Slack).\n\t\t\t// Resolve the credentials through the shell like other config\n\t\t\t// values so $VAR and $(cmd) work.\n\t\t\tvar preregistered *oauth.OAuthClient\n\t\t\tif strings.TrimSpace(m.OAuthClientID) != \"\" {\n\t\t\t\tclientID, err := resolver.ResolveValue(m.OAuthClientID)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, nil, fmt.Errorf(\"oauth_client_id: %w\", err)\n\t\t\t\t}\n\t\t\t\tclientSecret, err := resolver.ResolveValue(m.OAuthClientSecret)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, nil, fmt.Errorf(\"oauth_client_secret: %w\", err)\n\t\t\t\t}\n\t\t\t\tpreregistered = &oauth.OAuthClient{\n\t\t\t\t\tClientID:     strings.TrimSpace(clientID),\n\t\t\t\t\tClientSecret: strings.TrimSpace(clientSecret),\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Normalize trailing slash for PRM discovery compatibility.\n\t\t\tnormalizedURL := strings.TrimSuffix(url, \"/\")\n\t\t\toauthHandler, oauthErr := mcpoauth.NewHandler(name, normalizedURL, m.OAuthToken, preregistered, tokenSaver, mcpoauth.IsInteractive(ctx), m.OAuthCallbackPort)\n\t\t\tif oauthErr != nil {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"failed to create OAuth handler for mcp %q: %w\", name, oauthErr)\n\t\t\t}\n\t\t\tauthURLs.Set(name, oauthHandler)","sourceCodeStart":1075,"sourceCodeEnd":1111,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/agent/tools/mcp/init.go#L1075-L1111","documentation":"For OAuth-enabled HTTP MCP servers, the pre-registered oauth_client_id is resolved through the shell (supporting $VAR and $(cmd)). If the resolver errors, it is wrapped as 'oauth_client_id: <cause>'.","triggerScenarios":"OAuth MCP config with a non-empty OAuthClientID containing an unresolvable $VAR or failing $(cmd) substitution passed to resolver.ResolveValue.","commonSituations":"Client ID kept in a secrets env var that is not exported in the crush process; secret manager command missing from PATH; typo in variable name.","solutions":["Look at the wrapped cause to see which variable/command failed","Export the client ID env var before launching crush","Verify any $(cmd) secret-fetch command exists and exits 0","Or omit client_id and rely on dynamic client registration"],"exampleFix":"// before\nmcp api type http url '...' oauth oauth_client_id '$OAUTH_ID'   # OAUTH_ID unset\n// after\nexport OAUTH_ID=abc123  # or hardcode the public client id","handlingStrategy":"validation","validationCode":"id := os.Getenv(\"OAUTH_CLIENT_ID\")\nif strings.TrimSpace(id) == \"\" {\n    return errors.New(\"OAUTH_CLIENT_ID must be exported before starting crush\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Export OAuth client IDs in the shell profile or CI environment","Verify secret-manager subcommands ($(cmd)) are installed and succeed","Prefer dynamic client registration when a static ID is not required","Log resolution failures early with a config validation step"],"tags":["mcp","oauth","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"}