charmbracelet/crush · error

oauth token source: %w

Error message

oauth token source: %w

What it means

The oauthRoundTripper injects bearer tokens into SSE HTTP requests. Before sending, it asks the OAuth handler for a TokenSource; if that call errors (handler closed, no authorization performed, discovery failure), the error is wrapped as `oauth token source: ...` and the request is not sent.

Source

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

	if err != nil {
		return nil, err
	}

	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		if authErr := rt.handler.Authorize(req.Context(), req, resp); authErr != nil {
			return resp, nil
		}
		resp.Body.Close()
		return rt.doRequestWithToken(req.Clone(req.Context()))
	}

	return resp, nil
}

func (rt *oauthRoundTripper) doRequestWithToken(req *http.Request) (*http.Response, error) {
	ts, err := rt.handler.TokenSource(req.Context())
	if err != nil {
		return nil, fmt.Errorf("oauth token source: %w", err)
	}
	if ts != nil {
		token, err := ts.Token()
		if err == nil && token != nil {
			req.Header.Set("Authorization", "Bearer "+token.AccessToken)
		}
	}
	return rt.base.RoundTrip(req)
}

func mcpTimeout(m config.MCPConfig) time.Duration {
	if m.Timeout > 0 {
		return time.Duration(m.Timeout) * time.Second
	}
	// OAuth flows require user interaction in a browser, so use a
	// generous default to avoid timing out mid-auth.
	if m.OAuth {
		return 30 * time.Second

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Run the MCP OAuth login flow (e.g. `/mcp` in the TUI) to authorize the server, then retry
  2. Clear the stale `mcp.<name>.oauth_token` from global config and re-authenticate
  3. Verify the server advertises OAuth metadata (/.well-known/oauth-authorization-server) if you expect token-based auth
  4. Alternatively disable oauth for the server and pass an Authorization header directly via headers

Example fix

// before: no token available
mcp acme {
  type sse
  url "https://acme.com/sse"
  oauth true
}
// after: either login first, or use a static header
mcp acme {
  type sse
  url "https://acme.com/sse"
  headers {
    Authorization "Bearer $ACME_TOKEN"
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure the server has been authorized before issuing calls
tok, _ := loadSavedToken(name)
if tok == nil || tok.AccessToken == "" {
    return fmt.Errorf("run the OAuth login for MCP server %q first", name)
}

Type guard

func hasUsableToken(tok *oauth.Token) bool {
    return tok != nil && tok.AccessToken != ""
}

Try / catch

resp, err := runMCPTool(ctx, name, tool, args)
if err != nil && strings.Contains(err.Error(), "oauth token source") {
    if reauthErr := mcpLogin(ctx, name); reauthErr == nil {
        resp, err = runMCPTool(ctx, name, tool, args) // retry once after auth
    }
}

Prevention

When it happens

Trigger: An SSE MCP request flows through the round-tripper while the OAuth handler cannot produce a token source: the MCP server was never authorized (`/mcp` login not done), the handler's authorization metadata is missing, or the context is cancelled mid-flow.

Common situations: Server requires OAuth but Crush was started non-interactively so no browser flow ran; saved token revoked server-side leaving the handler unable to build a source; rapid requests racing handler shutdown during session teardown.

Related errors


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