[BUG] MCP OAuth: "Token expired without refresh token" despite refresh token being stored in Keychain
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Claude Code fails to use stored OAuth refresh tokens for HTTP MCP servers. The refresh token is correctly obtained during OAuth flow and persisted to macOS Keychain, but on the next session startup, Claude Code reports "Token expired without refresh token" and forces a full re-authentication.
This means users must manually re-authenticate via /mcp every ~24 hours (whenever the access token expires).
What Should Happen?
When an access token expires, Claude Code should read the stored refresh token from Keychain and use it to obtain a new access token silently, without user intervention.
Error Messages/Logs
Session startup (access token expired):
[DEBUG] MCP server "plugin:circleback:circleback": HTTP transport options: {"url":"https://app.circleback.ai/api/mcp","headers":{"User-Agent":"claude-code/2.1.39 (cli)"},"hasAuthProvider":true,"timeoutMs":60000}
[DEBUG] MCP server "plugin:circleback:circleback": Token expired without refresh token
[DEBUG] MCP server "plugin:circleback:circleback": Token expired without refresh token
[DEBUG] MCP server "plugin:circleback:circleback": Generated new OAuth state
[DEBUG] MCP server "plugin:circleback:circleback": HTTP Connection failed after 953ms: Unauthorized (code: none, errno: none)
[ERROR] MCP server "plugin:circleback:circleback" Error: Unauthorized
[DEBUG] MCP server "plugin:circleback:circleback": Authentication required for HTTP server
After manual re-auth (tokens are saved correctly):
[DEBUG] MCP server "plugin:circleback:circleback": Saving tokens
[DEBUG] MCP server "plugin:circleback:circleback": Token expires in: 86399
[DEBUG] MCP server "plugin:circleback:circleback": Has refresh token: true
[DEBUG] MCP server "plugin:circleback:circleback": Auth result: AUTHORIZED
Proof that refresh token IS stored in Keychain:
$ security find-generic-password -s "Claude Code-credentials" -w | python3 -c "
import json,sys
d=json.loads(sys.stdin.read())
for k,v in d.get('mcpOAuth',{}).items():
if 'circleback' in k:
print(f'hasRefreshToken: {bool(v.get(\"refreshToken\"))}')
print(f'expiresAt: {v.get(\"expiresAt\")}')
"
# Output:
# hasRefreshToken: True
# expiresAt: 1770978132539
Steps to Reproduce
- Install Circleback MCP plugin (HTTP type,
https://app.circleback.ai/api/mcp) - Authenticate via
/mcp→ Circleback → Authenticate - Verify tokens are saved: access token + refresh token both present in Keychain
- Wait >24 hours (or until access token expires)
- Start a new Claude Code session
- Observe: "Token expired without refresh token" in debug logs
- Claude Code requires full re-authentication instead of using the stored refresh token
Key Observation
This is not the same as #7744 (where scopes_supported is ignored and no refresh token is issued). In this case:
- The OAuth server (Circleback) does issue a refresh token
- Claude Code does save it to Keychain
- But Claude Code fails to read it back on the next session, reporting "without refresh token"
This is also not the same as #19456 (Keychain write failures after updates). There are no Keychain errors in the logs — the token simply isn't being loaded.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.39
Platform
Anthropic API (Max)
Operating System
macOS 15.3 (Sequoia)
Terminal/Shell
iTerm2
Additional Information
- Tested with Circleback MCP plugin (
type: "http") - Access token lifetime: 86400 seconds (24 hours)
- Scope:
user - The issue is reproducible on every session restart after token expiry
- Related issues: #7744, #19456, #12614 (closed)
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗