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.SecondView on GitHub (pinned to 7944b8e522)
Solutions
- Run the MCP OAuth login flow (e.g. `/mcp` in the TUI) to authorize the server, then retry
- Clear the stale `mcp.<name>.oauth_token` from global config and re-authenticate
- Verify the server advertises OAuth metadata (/.well-known/oauth-authorization-server) if you expect token-based auth
- 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
- Complete the /mcp OAuth login before scripted use of OAuth servers
- Fall back to static Authorization headers when OAuth metadata is absent
- Monitor for 401s and trigger re-auth proactively
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
- failed to create OAuth handler for mcp %q: %w
- failed to get MCP auth URL: status code %d
- interactive OAuth authorization required
- failed to start OAuth callback listener: all candidate ports
- OAuth callback listener closed
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/7b28c575b20df631.
Report an issue: GitHub.