[BUG] Custom MCP connector OAuth tokens fail to persist across restart; menu "Connect" silently fails while in-chat auth works
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?
Summary
After completing the OAuth flow for a custom MCP connector (Quarri: https://app.quarri.ai/mcp), the connection only works within a single in-session in-chat auth prompt. It does not persist across app restarts, and the Settings → Connectors → Connect button fails silently ("connected" shown but no tools load). Managed connectors (Gmail, Drive, Granola, Apollo) work correctly on the same device and account.
Environment
Windows 11, user tl
Claude Code CLI (version from claude --version)
Claude Desktop, Cowork mode
Timeline: Started ~April 22–23, 2026 (lines up with Claude Code 2.1.117/2.1.118 releases)
Custom MCP server: https://app.quarri.ai/mcp, HTTP transport, OAuth 2.1 + PKCE + Dynamic Client Registration
Server compliance verified (all passing)
✅ 401 response includes WWW-Authenticate: Bearer resource_metadata="https://app.quarri.ai/.well-known/oauth-protected-resource"
✅ /.well-known/oauth-protected-resource returns RFC 9728-compliant JSON
✅ /.well-known/oauth-authorization-server serves valid AS metadata
✅ DCR endpoint accepts registrations and omits optional fields rather than returning null (fixed after previous Zod v4 validation error)
Server is fully MCP 2025-06-18 spec compliant
Observed bugs
Desktop Settings → Connectors → Connect: Launches browser OAuth → completes successfully → returns to app → UI shows "connected" → but no tools are ever loaded. Silent failure.
No token persistence across app restart: Even when an in-chat auth flow succeeds and tools work in-session, closing and reopening the app reverts the connector to an unauthenticated state. Menu still claims "connected" but no tools are available.
Only reliable workaround: In-chat MCP auth prompt (Claude itself detects the need and triggers OAuth). Must be repeated every session.
Claude Code CLI behavior: claude mcp list reports ✗ Failed to connect rather than ! Needs authentication, because:
~/.claude/mcp-needs-auth-cache.json only contains managed connectors (e.g. claude.ai Google Calendar) — custom MCPs are never added
Stale DCR client registrations from earlier failed attempts persist in ~/.claude/.credentials.json under mcpOAuth (e.g. quarri|0c54185165cc81b2) and must be manually wiped to produce a clean auth state
Initial root cause (now fixed on server side): Claude Code SDK Zod v4 validation rejected null on optional RFC 7591 DCR fields (client_uri, logo_uri, software_id, software_version) where previous versions tolerated them. Relevant error:
SDK auth failed: [
{"expected":"string","code":"invalid_type","path":["client_uri"],"message":"Invalid input: expected string, received null"},
{"code":"invalid_union","path":["logo_uri"], ...},
{"expected":"string","code":"invalid_type","path":["software_id"], ...},
{"expected":"string","code":"invalid_type","path":["software_version"], ...}
]
Server was updated to omit these fields — but client-side persistence issues remain.
Likely root cause (Anthropic side)
Custom connectors use a different token storage / retrieval code path than managed connectors, and the former is not wired through to the restart/reconnect layer
401 response classification inconsistency: custom MCPs classified as "generic connect failure" instead of "needs auth," so re-auth is never prompted
Stale DCR client registrations from failed attempts are never cleaned up, causing compounding breakage
Reproduction
Add custom MCP via Settings → Connectors → Add custom connector → https://app.quarri.ai/mcp
Click Connect → complete OAuth → observe "connected"
Attempt to use a tool from the connector → tools don't appear
Trigger in-chat MCP prompt → auth succeeds → tools appear and work
Quit and reopen app → tools gone, menu still claims connected
Reference IDs
Intercom conversation: 215474031698397
Error session reference: ofid_5f63d6676526dbdd
What Should Happen?
Custom MCP connectors should behave the same as managed connectors (Gmail, Drive, Granola, Apollo) with respect to authentication and persistence. Specifically:
Settings → Connectors → Connect should, after successful OAuth, produce a working connection where tools are immediately available — not a silent "connected but no tools" state.
OAuth tokens for custom connectors should persist across app restarts. After authenticating once, closing and reopening Claude Desktop / Cowork / Claude Code should restore the authenticated session without requiring re-authentication, exactly as happens for managed connectors.
When a stored token expires or is invalid, the client should classify the resulting 401 as "Needs authentication" (not "Failed to connect") and prompt for re-auth automatically, using the RFC 9728 Protected Resource Metadata the server advertises via the WWW-Authenticate header. Custom MCPs should be added to mcp-needs-auth-cache.json just like managed connectors are.
Stale client registrations from prior failed OAuth attempts should be cleaned up automatically on subsequent retry, rather than persisting indefinitely in .credentials.json and requiring manual wipe of mcpOAuth entries to recover.
The in-chat auth flow and the Settings → Connectors auth flow should produce identical, equally durable results — there should not be a second-class path that only works in-session.
Error Messages/Logs
Error Messages / Logs
1. Initial OAuth handshake error (Zod v4 DCR validation — server side since fixed)
Shown in terminal via /mcp → quarri → Authenticate, during the ~April 22–23, 2026 window when Claude Code 2.1.117/2.1.118 shipped:
Quarri MCP Server
Status: ∆ needs authentication
Auth: × not authenticated
URL: https://app.quarri.ai/mcp
Config location: C:\Users\tl\.claude.json
Error: SDK auth failed: [
{
"expected": "string",
"code": "invalid_type",
"path": ["client_uri"],
"message": "Invalid input: expected string, received null"
},
{
"code": "invalid_union",
"errors": [
[{ "expected": "string", "code": "invalid_type", "path": [], "message": "Invalid input: expected string, received null" }],
[{ "code": "invalid_value", "values": [""], "path": [], "message": "Invalid input: expected \"\"" }]
],
"path": ["logo_uri"],
"message": "Invalid input"
},
{
"expected": "string",
"code": "invalid_type",
"path": ["software_id"],
"message": "Invalid input: expected string, received null"
},
{
"expected": "string",
"code": "invalid_type",
"path": ["software_version"],
"message": "Invalid input: expected string, received null"
}
]
> 1. Authenticate
2. Disable
This indicates the MCP SDK (likely Zod v4 migration in @modelcontextprotocol/sdk) began strictly validating RFC 7591 optional fields as required strings, rejecting null. These fields are per-spec optional and should be treated as such (accept null or undefined, or omit from schema). Fixed on the Quarri server side by omitting the fields — but the strict client schema remains a latent issue for any OAuth server returning null on optional DCR metadata.
2. Post-auth reconnection failure (current, reproducible)
After completing OAuth in the browser and returning to Claude Code:
> /mcp
⎿ Authentication successful, but server reconnection failed. You may need to manually restart Claude Code for the
changes to take effect.
Restarting Claude Code does not resolve it. Running claude mcp list afterward reports:
Checking MCP server health…
quarri: https://app.quarri.ai/mcp (HTTP) - ✗ Failed to connect
Note: status is ✗ Failed to connect, not ! Needs authentication, despite the server returning a 401 with a valid WWW-Authenticate header indicating OAuth is expected.
3. Desktop / Cowork in-app error
Shown in Claude Desktop chat after calling a quarri tool post-restart:
Couldn't reach the MCP server. You can check the server URL and verify the server is running.
If this persists, share this reference with support: "ofid_5f63d6676526dbdd"
Server is reachable; issue is client-side token binding.
4. Direct server probe (confirms server is compliant, client is at fault)
$ curl -i -X POST https://app.quarri.ai/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}'
HTTP/1.1 401 UNAUTHORIZED
server: Fly/0e7dad54f (2026-04-23)
content-type: application/json
www-authenticate: Bearer resource_metadata="https://app.quarri.ai/.well-known/oauth-protected-resource"
...
{"error":{"code":-32000,"message":"Missing or invalid Authorization header"},"id":null,"jsonrpc":"2.0"}
$ curl -i https://app.quarri.ai/.well-known/oauth-protected-resource
HTTP/1.1 200 OK
content-type: application/json
{
"authorization_servers": ["https://app.quarri.ai"],
"bearer_methods_supported": ["header"],
"resource": "https://app.quarri.ai/mcp",
"resource_documentation": "https://app.quarri.ai/docs",
"scopes_supported": []
}
$ curl https://app.quarri.ai/.well-known/oauth-authorization-server
{
"authorization_endpoint": "https://app.quarri.ai/oauth/authorize",
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code"],
"issuer": "https://app.quarri.ai",
"registration_endpoint": "https://app.quarri.ai/oauth/register",
"response_types_supported": ["code"],
"service_documentation": "https://app.quarri.ai/docs",
"token_endpoint": "https://app.quarri.ai/oauth/token",
"token_endpoint_auth_methods_supported": ["none"]
}
Server is fully MCP 2025-06-18 / RFC 9728 / RFC 8414 compliant. Despite this, the Claude Code client classifies the 401 as a connect failure rather than an auth challenge.
5. Stale DCR client registration in credential store
Evidence of state pollution from failed OAuth attempts:
$ cat ~/.claude/.credentials.json | jq 'keys, .mcpOAuth | keys'
[
"claudeAiOauth",
"mcpOAuth"
]
[
"quarri|0c54185165cc81b2"
]
The quarri|0c54185165cc81b2 entry remains even after:
claude mcp remove quarri -s user
claude mcp add ... re-adding the server
Repeated OAuth completions
No automatic cleanup of failed or superseded client registrations. Required manual node-script wipe to recover a clean auth state.
6. Auth-cache classification gap
$ cat ~/.claude/mcp-needs-auth-cache.json
{"claude.ai Google Calendar":{"timestamp":1776973847941}}
Only managed connectors are ever written here. quarri never appears despite:
Returning a spec-correct 401 + WWW-Authenticate header
The user having completed OAuth for it multiple times
The stored OAuth token existing under mcpOAuth in .credentials.json
This is almost certainly the root-cause data structure: since quarri isn't in this cache, the CLI does not classify its 401s as auth challenges and does not trigger re-auth, leading to the ✗ Failed to connect status and the silent "connected but no tools" desktop state.
7. Timeline summary
When Claude Code Behavior
Pre April 22, 2026 2.1.116 or earlier Custom connector worked, persisted across restarts
April 22–23, 2026 2.1.117 → 2.1.118 Zod v4 DCR validation error; total auth failure
After server DCR fix 2.1.118+ OAuth completes but token not bound on reconnect; silent "connected but no tools" via Desktop menu; in-chat auth works only in-session; no persistence across restart
Steps to Reproduce
Prerequisites
Claude Code 2.1.117 or later (desktop app and/or CLI)
A custom MCP server with OAuth 2.1 + Dynamic Client Registration that is fully MCP 2025-06-18 spec compliant. The Quarri MCP server at https://app.quarri.ai/mcp can be used directly; it currently passes all spec checks:
Verifies server compliance (all should return expected values)
curl -i -X POST https://app.quarri.ai/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}'
→ HTTP 401 with header: www-authenticate: Bearer resource_metadata="https://app.quarri.ai/.well-known/oauth-protected-resource"
curl -i https://app.quarri.ai/.well-known/oauth-protected-resource
→ HTTP 200, RFC 9728 JSON body
curl https://app.quarri.ai/.well-known/oauth-authorization-server
→ Valid OAuth 2.0 AS metadata JSON
Reproduction A — Settings → Connectors menu silently fails
Open Claude Desktop.
Go to Settings → Connectors → Add custom connector.
Enter URL: https://app.quarri.ai/mcp, name: quarri. Save.
Click Connect next to the new quarri entry.
Browser opens, OAuth flow completes successfully, browser shows "You're connected, return to Claude".
Return to Claude Desktop. Menu shows quarri as Connected.
Open a new chat and ask Claude to use a quarri tool (e.g. "list my databases").
Observed: No quarri tools are available. Claude reports the connector is unavailable, despite the menu showing "Connected".
Expected: Quarri tools appear and are callable.
Reproduction B — Token does not persist across restart
Complete Reproduction A, then trigger the in-chat MCP auth flow instead (prompt Claude to use a quarri tool; when it errors, follow the in-chat "Authenticate" prompt).
Complete OAuth in browser. Return to Claude. Tools now load and work. Call list_databases or similar — succeeds.
Fully quit Claude Desktop (task tray → Quit, or Cmd/Ctrl+Q). Verify process is gone.
Reopen Claude Desktop. Open a new chat. Ask Claude to call any quarri tool.
Observed: Tools are no longer available. Settings → Connectors still shows quarri as "Connected", but calling any tool fails. No re-auth prompt appears.
Expected: Tools available without re-auth, matching the behavior of managed connectors (Gmail, Drive) on the same device.
Reproduction C — CLI misclassifies 401 and accumulates stale state
In PowerShell / terminal:
claude mcp add --transport http --scope user quarri https://app.quarri.ai/mcp
claude mcp list
Observe: quarri: https://app.quarri.ai/mcp (HTTP) - ! Needs authentication ✓ (correct)
Run claude, then inside the session run /mcp → select quarri → Authenticate. Complete OAuth in browser. Observe terminal output: Authentication successful, but server reconnection failed. You may need to manually restart Claude Code for the changes to take effect.
/exit, then in PowerShell:
claude mcp list
Observed: quarri: https://app.quarri.ai/mcp (HTTP) - ✗ Failed to connect — NOT ! Needs authentication.
Inspect state:
type $env:USERPROFILE\.claude\mcp-needs-auth-cache.json
→ only contains managed connectors (e.g. "claude.ai Google Calendar"); quarri is absent
node -e "const c=require('fs').readFileSync(process.env.USERPROFILE+'/.claude/.credentials.json','utf8'); const j=JSON.parse(c); console.log(Object.keys(j.mcpOAuth||{}))"
→ shows stale entries like "quarri|0c54185165cc81b2" from failed DCR attempts that were never cleaned up
Expected: After a successful auth, claude mcp list should show ✓ connected. On token expiry, it should show ! Needs authentication and add quarri to mcp-needs-auth-cache.json, mirroring the managed-connector code path.
Reproduction D — Only the in-chat path produces a working connection (demonstrates divergent code paths)
Fresh state: claude mcp remove quarri -s user, wipe mcpOAuth entries in .credentials.json matching ^quarri|, delete any Quarri entry from the Desktop Connectors UI.
Variant A: Re-add via Settings → Connectors → Add custom connector, click Connect. Complete OAuth.
Variant B: Re-add via claude mcp add --transport http --scope user quarri https://app.quarri.ai/mcp, launch claude, let in-chat flow prompt for auth, complete OAuth.
Observed: Variant A produces "connected but no tools" state. Variant B produces a working in-session connection but does not survive restart (see Reproduction B).
Expected: Both variants produce identical, persistent, working connections.
Comparison control
On the same device, same user account, managed connectors (claude.ai Gmail, claude.ai Google Drive, claude.ai Granola, Apollo) work correctly through Reproduction A/B/C equivalents. They persist across restarts, correctly classify expired tokens as "Needs authentication," and are populated in mcp-needs-auth-cache.json. The bug is specific to custom connectors.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
2.1.116 or earlier
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Other
Additional Information
_No response_
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗