charmbracelet/crush · error

oauth_client_id: %w

Error message

oauth_client_id: %w

What it means

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>'.

Source

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

		// on every exchange and refresh via this saver.
		if m.OAuth {
			tokenSaver := func(tok *oauth.Token) {
				if err := cfg.SetConfigField(config.ScopeGlobal, fmt.Sprintf("mcp.%s.oauth_token", name), tok); err != nil {
					slog.Warn("Failed to persist MCP OAuth token", "name", name, "error", err)
				} else {
					slog.Info("Persisted MCP OAuth token", "name", name)
				}
			}

			// A pre-registered client is required for servers that do not
			// support dynamic client registration (e.g. GitHub, Slack).
			// Resolve the credentials through the shell like other config
			// values so $VAR and $(cmd) work.
			var preregistered *oauth.OAuthClient
			if strings.TrimSpace(m.OAuthClientID) != "" {
				clientID, err := resolver.ResolveValue(m.OAuthClientID)
				if err != nil {
					return nil, nil, fmt.Errorf("oauth_client_id: %w", err)
				}
				clientSecret, err := resolver.ResolveValue(m.OAuthClientSecret)
				if err != nil {
					return nil, nil, fmt.Errorf("oauth_client_secret: %w", err)
				}
				preregistered = &oauth.OAuthClient{
					ClientID:     strings.TrimSpace(clientID),
					ClientSecret: strings.TrimSpace(clientSecret),
				}
			}

			// Normalize trailing slash for PRM discovery compatibility.
			normalizedURL := strings.TrimSuffix(url, "/")
			oauthHandler, oauthErr := mcpoauth.NewHandler(name, normalizedURL, m.OAuthToken, preregistered, tokenSaver, mcpoauth.IsInteractive(ctx), m.OAuthCallbackPort)
			if oauthErr != nil {
				return nil, nil, fmt.Errorf("failed to create OAuth handler for mcp %q: %w", name, oauthErr)
			}
			authURLs.Set(name, oauthHandler)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Look at the wrapped cause to see which variable/command failed
  2. Export the client ID env var before launching crush
  3. Verify any $(cmd) secret-fetch command exists and exits 0
  4. Or omit client_id and rely on dynamic client registration

Example fix

// before
mcp api type http url '...' oauth oauth_client_id '$OAUTH_ID'   # OAUTH_ID unset
// after
export OAUTH_ID=abc123  # or hardcode the public client id
Defensive patterns

Strategy: validation

Validate before calling

id := os.Getenv("OAUTH_CLIENT_ID")
if strings.TrimSpace(id) == "" {
    return errors.New("OAUTH_CLIENT_ID must be exported before starting crush")
}

Prevention

When it happens

Trigger: OAuth MCP config with a non-empty OAuthClientID containing an unresolvable $VAR or failing $(cmd) substitution passed to resolver.ResolveValue.

Common situations: 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.

Related errors


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