claude-code - 💡(How to fix) Fix [BUG] Claude Desktop and Claude Code CLI never re-register MCP tools after OAuth 2.1 handshake on a remote HTTP server

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

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

Fix Action

Fix / Workaround

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.

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).

Code Example

### 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.

---

# 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

---

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

---

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>.hello200, isError: false
POST /oauth/token grant_type=refresh_token             → 200, new pair issued, old access revoked

---

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
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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 curl walk-through of the entire OAuth + MCP flow (every step succeeds, every response shape conforms),
    • the official MCP Inspector (@modelcontextprotocol/inspector v0.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/sdk v0.5.0 → v0.4.0 (transitively also symfony/mcp-bundle v0.9.0 → v0.8.0, and phpdocumentor/reflection-docblock 6→5 + type-resolver 2→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:

  1. The pre-auth virtual tools should disappear from the session — this part works.
  2. The client should issue an MCP initialize JSON-RPC call against the server with the freshly-obtained Authorization: Bearer <access_token> header. The server responds with 200 OK carrying Mcp-Session-Id: <uuid>, capabilities, serverInfo, and the negotiated protocolVersion: "2025-06-18".
  3. The client should issue notifications/initialized (replied with 202 Accepted).
  4. The client should issue tools/list — the server returns the full tool catalog.
  5. 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:

  1. 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
    The local callback server is listening on localhost:<port>.
  2. User opens the URL in a system browser, completes SSO + consent.
  3. Browser is redirected to http://localhost:<port>/callback?code=<code>&state=<state> — the local callback server consumes the code.
  4. Client exchanges the code at POST /oauth/token and gets back { access_token, refresh_token, expires_in: 3600, token_type: "Bearer" }. Token rotation works correctly on subsequent refreshes.
  5. The two pre-auth virtual tools disappear from the session (correct).
  6. 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

FieldValue
OS (client)Windows 11
Claude Desktop (Electron shell)1.9255.2
Embedded Claude Code (CCD)2.1.149
Node embedded in CCD24.15.0
Claude Code CLI (standalone, Linux)2.1.152 (also reproducing)
MCP transportremote http (Streamable HTTP, JSON-RPC 2.0)
MCP server URL patternhttps://<mcp-server-host>/
AuthOAuth 2.1 + PKCE S256, Dynamic Client Registration (RFC 7591)
Client config location~/.claude.jsonmcpServers.<name> (type http)

Related

  • #62298 (Windows, May 25 2026) — same broader class of bug (MCP post-OAuth broken on http transport), but a different failure mechanism: there, the mcp-needs-auth-cache.json is 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-auth tools/list handshake 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/token headers carry Cache-Control: no-cache, private instead of the RFC 6749 §5.1 mandated no-store, and the Pragma: no-cache header is missing. No major OAuth client enforces these in practice. Both curl and 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/sdk v0.5.0 (OAuth middleware refactor, Tool::__construct BC break, Session::save() fix)
  • symfony/mcp-bundle v0.9.0 (TraceableRegistry ResetInterface removal)
  • phpdocumentor/reflection-docblock 6.x + type-resolver 2.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

  1. 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.
  2. 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.
  3. 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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING