[BUG] MCP OAuth: token exchange silently fails for Robinhood MCP servers -- empty accessToken persisted, empty error from complete_authentication (manual exchange with identical params succeeds)

Resolved 💬 2 comments Opened Jun 6, 2026 by pd-product Closed Jun 10, 2026

Preflight Checklist

  • [x] I have searched existing issues -- closest are #65656, #59725, #60260 (same symptom family); this report adds root-cause evidence from a manual reproduction of the exchange
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code (2.1.167)

What's Wrong?

Native MCP OAuth against Robinhood's MCP servers (https://agent.robinhood.com/mcp/trading, https://banking-agent.robinhood.com/mcp/banking, both type: http) never produces a usable token. The browser authorize leg succeeds every time, but Claude Code persists "accessToken": "" (empty string) into ~/.claude/.credentials.json under mcpOAuth, and the server stays in "needs authentication".

Two failure presentations:

  1. Remote session (paste-callback path): complete_authentication returns Authentication failed for robinhood-trading: -- error detail is empty, hiding the real failure.
  2. Local session (localhost listener path): flow appears to succeed silently, but the stored accessToken is still "".

Evidence the failure is in Claude Code's exchange/persist step, not the provider

I reproduced the exact flow in a standalone Python script: same client_id (from Claude Code's own dynamic registration), same authorize endpoint, same token endpoint, PKCE S256, token_endpoint_auth_method: none, same resource parameter -- and it succeeds:

  • POST https://api.robinhood.com/oauth2/token/ returns 200 with a valid Bearer token (expires_in ~344000) + refresh token
  • The token works: MCP initialize against the server returns 200
  • Manually writing that token into the existing mcpOAuth entry in .credentials.json fully restores function -- after restart both servers list and serve all tools

One provider quirk that may be relevant to the parser: Robinhood's token response includes non-standard extra fields alongside the standard ones:

keys: ['access_token', 'backup_code', 'expires_in', 'mfa_code', 'refresh_token', 'scope', 'token_type', 'user_uuid']

Provider metadata details (path-inserted RFC 8414 form; the suffix form 404s):

  • https://agent.robinhood.com/.well-known/oauth-authorization-server/mcp/trading
  • token endpoint https://api.robinhood.com/oauth2/token/, grant types authorization_code + refresh_token, PKCE S256, auth method none
  • scopes differ per server: trading internal, banking credit-card

What Should Happen?

  1. The token exchange should succeed (it does when driven manually with identical parameters), and the access token should be persisted.
  2. If the exchange fails, the error message should include the failure detail -- Authentication failed for <server>: with nothing after the colon makes diagnosis impossible.
  3. An empty-string token should arguably never be persisted as if auth completed.

Steps to Reproduce

  1. claude mcp add --transport http robinhood-trading https://agent.robinhood.com/mcp/trading (requires a Robinhood account)
  2. Run the authenticate tool, complete the browser flow (succeeds)
  3. Observe: server still needs auth; ~/.claude/.credentials.json mcpOAuth entry has "accessToken": ""
  4. Paste-callback variant: complete_authentication fails with empty error detail

Environment

  • Claude Code 2.1.167, native Windows terminal (PowerShell), Windows 11 Pro
  • Reproduced across multiple sessions and multiple days (entries for both servers show the same empty-token state from independent attempts)

Claude Model

Opus 4.8

Is this a regression?

I don't know -- first attempt to use these servers.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗