[BUG] Claude Desktop and Claude Code CLI never re-register MCP tools after OAuth 2.1 handshake on a remote HTTP server
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?
After a successful OAuth 2.1 + PKCE S256 authorization flow against a remote http-transport MCP server, both Claude Desktop and Claude Code CLI silently fail to register the post-auth tool catalog.
- The two pre-auth virtual tools (
mcp__<name>__authenticate,mcp__<name>__complete_authentication) disappear from the session as expected after the OAuth completes, signalling that the client correctly observed the auth state transition. - However, the real tool catalog advertised by the server's
tools/list(24 tools in our case, across multiple providers) is never exposed to the agent. - The user ends up with zero
mcp__<name>__*tools usable in the session for that server. - A clean restart (quit Claude Desktop + kill all processes + purge cached OAuth credentials in
~/.claude.json+ relaunch) does not restore the behaviour.
We have spent significant time eliminating the server side as a cause:
- The server has been validated as strictly conformant to OAuth 2.1, RFC 6749 §5.1, RFC 8414, RFC 9728, RFC 7591, and the MCP spec
2025-06-18, from three independent angles: - manual
curlwalk-through of the entire OAuth + MCP flow (every step succeeds, every response shape conforms), - the official MCP Inspector (
@modelcontextprotocol/inspectorv0.21.2), - a line-by-line RFC audit of every response (body and headers).
- Two targeted dependency rollbacks of the server side were applied to production and re-tested with Claude Desktop:
- Round 1: roll back
mcp/sdkv0.5.0 → v0.4.0 (transitively alsosymfony/mcp-bundlev0.9.0 → v0.8.0, andphpdocumentor/reflection-docblock6→5 +type-resolver2→1). → No change. - Round 2: roll back every Symfony 7.4.9..13 component to 7.4.8 (35 components, reintroducing 20 CVEs flagged by
composer audit, accepted strictly for the bisect). → No change.
Both rollbacks were reverted afterwards. Neither matched the cause.
The remaining never-rolled-back deltas from the same composer bump (AWS SDK patch, Doctrine ORM patch, Guzzle patches, Twig minor, webmozart/assert minor) are all outside the inbound MCP request path and are not plausible candidates.
Conclusion: the cause is client-side, in the code path that should re-handshake an http-transport MCP server after a successful OAuth dance and call tools/list on it.
The fact that the affected server is silently dropped from the very first replaceRemoteMcpServers call — before any per-server log line is emitted — suggests the drop happens during initial config parsing or server-bootstrap validation, possibly tied to recent client-side hardening on http-transport MCP servers using OAuth 2.1 Dynamic Client Registration.
What Should Happen?
After the user completes the OAuth + consent flow in the system browser and the callback is consumed by the embedded local callback server:
- The pre-auth virtual tools should disappear from the session — this part works.
- The client should issue an MCP
initializeJSON-RPC call against the server with the freshly-obtainedAuthorization: Bearer <access_token>header. The server responds with200 OKcarryingMcp-Session-Id: <uuid>, capabilities,serverInfo, and the negotiatedprotocolVersion: "2025-06-18". - The client should issue
notifications/initialized(replied with202 Accepted). - The client should issue
tools/list— the server returns the full tool catalog. - The agent should now have the real tools (
mcp__<name>__<tool>) usable in the same session, without requiring a restart.
curl reproduces the full sequence successfully against our production server with any freshly-issued Bearer token.
Error Messages/Logs
### Claude Desktop (`%APPDATA%\Claude\logs\main.log`)
[CCD] [replaceRemoteMcpServers] Calling SDK with 9 total servers
[CCD] LocalSessions.replaceRemoteMcpServers: sessionId=local_…, serverCount=2
- 9 servers are configured in `~/.claude.json`; only 2 reach the local session.
- The affected server is among the 7 silently dropped between those two log lines.
- **No log line containing the affected server's name or hostname appears anywhere in `main.log` during boot.** Whatever filter drops it does not log its reason, which is the main blocker for self-diagnosis.
### Claude Code CLI (interactive session, Linux)
The agent observes the pre-auth tools disappear after the user pastes the OAuth callback URL, then no further tool registration happens. No structured error is surfaced in stdout; the MCP server simply reports as disconnected from the client's perspective shortly after.
Calling `claude mcp list` afterwards rapports `✓ Connected` (the OAuth tokens are cached, the credentials are valid), but the in-session tool catalogue stays empty.
### Server side
`POST /` (with the access token issued during the failed Desktop / CLI session) returns the full conformant `initialize` response when called from `curl`. The server's access logs show **no** `initialize` request from the Claude client after the `/oauth/token` exchange — the client never tries.
Steps to Reproduce
# On Linux with the standalone CLI (also reproducible on Claude Desktop on Windows)
claude mcp add --transport http -s user <name> https://<mcp-server-host>/
claude mcp list
# → <name>: https://<mcp-server-host>/ (HTTP) - ! Needs authentication
claude # start interactive session
In the session:
- Agent invokes
mcp__<name>__authenticate. It returns an authorization URL like:
```
https://<mcp-server-host>/oauth/authorize?response_type=code&client_id=<id>&code_challenge=<S256>&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A<port>%2Fcallback&state=<state>&resource=https%3A%2F%2F<mcp-server-host>%2F
localhost:<port>`.
The local callback server is listening on
- User opens the URL in a system browser, completes SSO + consent.
- Browser is redirected to
http://localhost:<port>/callback?code=<code>&state=<state>— the local callback server consumes the code. - Client exchanges the code at
POST /oauth/tokenand gets back{ access_token, refresh_token, expires_in: 3600, token_type: "Bearer" }. Token rotation works correctly on subsequent refreshes. - The two pre-auth virtual tools disappear from the session (correct).
- Observed: no real tools surface. The session is left with zero
mcp__<name>__*tools.
For comparison, the same flow done outside the client (manual curl with the same Bearer token issued in step 4) succeeds end-to-end and returns the full tools/list of 24 tools.
Claude Model
None
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
2.1.149
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Terminal.app (macOS)
Additional Information
Environment
| Field | Value |
|---|---|
| OS (client) | Windows 11 |
| Claude Desktop (Electron shell) | 1.9255.2 |
| Embedded Claude Code (CCD) | 2.1.149 |
| Node embedded in CCD | 24.15.0 |
| Claude Code CLI (standalone, Linux) | 2.1.152 (also reproducing) |
| MCP transport | remote http (Streamable HTTP, JSON-RPC 2.0) |
| MCP server URL pattern | https://<mcp-server-host>/ |
| Auth | OAuth 2.1 + PKCE S256, Dynamic Client Registration (RFC 7591) |
| Client config location | ~/.claude.json → mcpServers.<name> (type http) |
Related
- #62298 (Windows, May 25 2026) — same broader class of bug (MCP post-OAuth broken on
httptransport), but a different failure mechanism: there, themcp-needs-auth-cache.jsonis not cleaned up after a successful auth, so the client keeps re-flagging the server as! Needs authentication. Here, the cache is cleaned (the pre-auth virtual tools disappear, signalling the client knows the OAuth succeeded), yet the post-authtools/listhandshake never happens. Same area of the code path is likely involved.
Appendix A — Server-side conformance proofs (so you can skip triaging this)
Manual curl walk-through (production server, real tokens)
POST /oauth/register → 201, client_id issued
GET /.well-known/oauth-authorization-server → 200, RFC 8414 conformant
GET /.well-known/oauth-protected-resource → 200, RFC 9728 conformant
GET /oauth/authorize?… → 200, consent page
POST /oauth/consent … → 302 with auth code
POST /oauth/token grant_type=authorization_code → 200, access_token + refresh_token + expires_in: 3600
POST / initialize → 200
Mcp-Session-Id: <uuid>
protocolVersion: "2025-06-18"
capabilities: {logging, completions, prompts.listChanged, resources.listChanged, tools.listChanged}
serverInfo: {name, version, description, icons}
POST / notifications/initialized → 202
POST / tools/list → 200, 24 tools returned (full inputSchema, descriptions, names)
POST / tools/call <name>.hello → 200, isError: false
POST /oauth/token grant_type=refresh_token → 200, new pair issued, old access revoked
CORS headers emitted on POST / (so it works even from browser-style clients)
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
Access-Control-Allow-Headers: Accept,Authorization,Content-Type,Last-Event-ID,Mcp-Protocol-Version,Mcp-Session-Id
Access-Control-Expose-Headers: Mcp-Session-Id
Known minor non-conformance (not blocking, flagged for honesty)
POST /oauth/tokenheaders carryCache-Control: no-cache, privateinstead of the RFC 6749 §5.1 mandatedno-store, and thePragma: no-cacheheader is missing. No major OAuth client enforces these in practice. Bothcurland MCP Inspector are happy with the response.
Appendix B — Bisect attempts already done
Round 1 — MCP SDK rollback (mcp/sdk v0.5.0 → v0.4.0)
Reasoning: mcp/sdk v0.5.0 (released 2026-04-26) shipped a refactor of OAuth2 to a middleware-based architecture, an Add top-level title to Tool schema-level change, a relax StrictOidcDiscoveryMetadataPolicy + Dynamic Client Registration middleware (RFC 7591) change, and a Session::save() lazy-init fix. All plausible candidates for breaking a strict client. Composer also transitively downgraded symfony/mcp-bundle v0.9.0 → v0.8.0 and phpdocumentor/reflection-docblock 6.x → 5.x + type-resolver 2.x → 1.x.
Result: no change. Claude Desktop still drops the server during replaceRemoteMcpServers.
Round 2 — Symfony 7.4 patches rollback (every 7.4.9..13 → 7.4.8)
Reasoning: the 7.4.9..7.4.13 patch series contained CVE fixes touching http-kernel, security-http (notably PR #64181 HttpUtils::createRequest() URL rebuild), framework-bundle (PR #64189 argument_resolver priority change), security-bundle (HEAD method bypass fix), serializer, http-foundation, mime, cache, runtime. Any of these could plausibly change response shape or middleware ordering. 35 components downgraded; 20 CVEs deliberately reintroduced for the bisect via composer --no-security-blocking (then reverted).
Result: no change. Claude Desktop still drops the server.
Suspects eliminated by Rounds 1 + 2 combined
mcp/sdkv0.5.0 (OAuth middleware refactor,Tool::__constructBC break,Session::save()fix)symfony/mcp-bundlev0.9.0 (TraceableRegistryResetInterfaceremoval)phpdocumentor/reflection-docblock6.x +type-resolver2.x (used by the SDK to reflect tool annotations into schemas)- All Symfony 7.4.9..7.4.13 component patches
Remaining unrolled deltas from the same server-side composer bump: AWS SDK patch, Doctrine ORM patch, Guzzle patches, Twig 3.24 → 3.26, webmozart/assert 2.3 → 2.4. None of these sit on the inbound MCP request path.
---
What we would need from Anthropic
- Confirmation that the symptom matches a known client-side regression in the
http-transport + OAuth 2.1 code path, ideally with rough timing of when it was introduced. - If unknown: a debug switch (env var, CLI flag, log level) to surface the silent drop reason in
replaceRemoteMcpServers— currently the absence of any log line for the dropped server is the main blocker for self-diagnosis. - A reproducible test case or a fix.
Full conformance traces, server access logs around the failing handshakes, and a screen recording of the symptom can be provided on request.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗