anthropics / anthropics/claude-code
[BUG] Desktop 1.32352.x: first-time OAuth MCP connect never opens the browser — version-negotiation probe wraps a client-side auth error, which is then misclassified as transport
- Lingua principale
- Python
- Stelle
- 145k
- Fork
- 23.1k
- Metriche di merge delle PR
- Metriche PR in attesa
Descrizione
## Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version
(Searched for `version negotiation`, `open spawn`, `parked for renderer`, `EraNegotiationFailed`. Closest prior art is #52565 — same *class* of mistake, 401 misclassified as a generic connect failure — but that is the CLI on Windows and is closed as not planned. #46640, #46623 and #48670 are adjacent but different mechanisms.)
---
## What's Wrong?
On Claude Desktop **1.32352.0 / 1.32352.1**, connecting an HTTP MCP connector that requires OAuth **for the first time** (no cached tokens) fails with "Connection to server failed. You can try again." in the UI. The browser is never opened, so there is no way for the user to complete authorization — clicking Connect can never succeed.
This worked in **1.25927.0** on the same machine and the same server config.
The server side is fully correct and verifiable independently: discovery metadata is served over HTTPS, the advertised `resource` matches the configured URL exactly, PKCE `S256` and `authorization_code` + `refresh_token` are advertised, and **dynamic client registration succeeds** — the log shows `registered OAuth client { clientId: … }`.
### Mechanism
The new version-negotiation probe fails with a *client-side* auth error — the auth provider has no cached token, so it throws before any request is sent:
```
Version negotiation probe failed: : OAuth needs interactive authorization (no cached tokens)
```
The probe's result classifier has a dedicated `auth-required` case that passes the typed error through untouched, and a `network-error` case that wraps it:
```js
case 'auth-required': return { kind: 'error', error: e.error }; // preserves typed error
case 'network-error': return OJe(e.error, t); // wraps as EraNegotiationFailed
```
A client-side "needs interactive authorization" is not an HTTP 401/403, so it lands in the `network-error` bucket and is wrapped as `SdkHttpError(EraNegotiationFailed)`. The wrapper does preserve the original as `{ cause: e }`.
The connect-path classifier then only does `instanceof` checks on the **outer** error and never inspects `.cause`:
```js
function YE(e,t){ return e instanceof LE ? 'auth-required'
: e instanceof IE ? 'keychain-locked'
: … : 'transport' }
```
So `auth-required` becomes `transport`, which takes the anonymous-fallback branch:
```js
if (!e.oauth && r !== 'auth-required' && r !== 'keychain-locked')
→ 'default OAuth probe failed — falling back to open spawn' → ZE(e, n, {anonymous:true})
```
That connects with no credentials, the server correctly 401s, and the state map sends `transport` → `transport-closed`. Because only `needs-auth` surfaces the authorize flow, **`needs authorization — parked for renderer` never fires and `opening authorize URL` is never reached.**
This is consistent with the MCP TypeScript SDK's stated contract, which is explicit that an auth wall must not be treated as a negotiation failure — but that contract is written in terms of server-side HTTP status codes, and this path is a client-side auth-provider throw, which has no branch in the probe's classifier:
> "Auth statuses are another exception: an HTTP `401` or `403` rejecting the probe is never era evidence — `connect()` rejects with a typed authorization failure."
> — https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28
Only **first-time** authorization is affected. Servers with cached tokens take the refresh path and connect normally, which is likely why this hasn't been widely reported.
---
## What Should Happen?
The same as on 1.25927.0: a server needing interactive authorization should be classified `auth-required`, logged as `needs authorization — parked for renderer`, and clicking Connect should open the authorize URL in the browser and complete the OAuth flow.
Working sequence from 1.25927.0, same server:
```
[custom3p-mcp] registered OAuth client { server: , clientId: … }
[custom3p-mcp] needs authorization — parked for renderer { name: }
[custom3p-mcp] opening authorize URL { server: , host: …, state: … }
[custom3p-mcp] waiting for OAuth callback (browser opened) { server: }
[custom3p-mcp] saved OAuth tokens { server: , hasRefresh: true, ttlSec: 3600 }
[custom3p-mcp] server connected { name: , toolCount: 15 }
```
Suggested fixes, either of which is sufficient:
1. Unwrap `.cause` in the connect-path classifier so a wrapped typed auth error is still recognised as `auth-required`.
2. Give the probe's classifier a branch for client-side auth-provider failures, so it returns `{kind:'error', error: }` rather than routing through the `network-error` wrapper.
As an immediate mitigation, the probe appears to be gated behind a feature flag (`hE()` → `dE('1942337209')` in the bundle); disabling it restores the working path, since the `{}` branch omits `versionNegotiation` entirely.
---
## Error Messages/Logs
```
2026-08-18 08:01:01 [info] [updater] Version changed since last launch: 1.25927.0 → 1.32352.0
2026-08-18 08:01:03 [info] [custom3p-mcp] registered OAuth client { server: '', clientId: '' }
2026-08-18 08:01:03 [info] [custom3p-mcp] default OAuth probe failed — falling back to open spawn { name: '', error: 'Version negotiation probe failed: : OAuth needs interactive authorization (no cached tokens)' }
2026-08-18 08:01:04 [error] [custom3p-mcp] connect failed — parked for retry { name: '', error: 'Error POSTing to endpoint: {"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Authentication required"}}' }
2026-08-18 08:01:04 [info] [custom3p-mcp] reconcile [managed-config,org-plugin]: +0 connected, +1 pending in 2025ms
# after clicking Connect in Settings → Customize → Connectors
2026-08-18 08:01:17 [info] LocalAgentModeSessions.authorizeDirectMcpServer: — reconnecting (http)
2026-08-18 08:01:17 [info] [custom3p-mcp] default OAuth probe failed — falling back to open spawn { name: '', error: 'Version negotiation probe failed: : OAuth needs interactive authorization (no cached tokens)' }
2026-08-18 08:01:18 [error] LocalAgentModeSessions.authorizeDirectMcpServer: — failed: Error POSTing to endpoint: {"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Authentication required"}}
# identical after updating to 1.32352.1
2026-08-18 08:29:00 [info] [custom3p-mcp] default OAuth probe failed — falling back to open spawn { name: '', error: 'Version negotiation probe failed: : OAuth needs interactive authorization (no cached tokens)' }
2026-08-18 08:29:03 [error] [custom3p-mcp] connect failed — parked for retry { name: '', error: 'Error POSTing to endpoint: {"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Authentication required"}}' }
```
Note `needs authorization — parked for renderer` and `opening authorize URL` never appear.
---
## Steps to Reproduce
1. Claude Desktop **1.32352.0 or 1.32352.1** on macOS, with third-party inference configured (so the `custom3p-mcp` host is in use).
2. Configure an HTTP MCP connector pointing at an **OAuth-protected MCP server that supports discovery + DCR** — either via Managed MCP servers in the managed config, or an org plugin's `.mcp.json`:
```json
{ "mcpServers": { "my-metabase": { "type": "http", "url": "https://metabase.example.com/api/metabase-mcp" } } }
```
A self-hosted Metabase 0.63.x with its native MCP server reproduces it. Any OAuth+DCR MCP server should.
3. Ensure there are **no cached tokens** for that server name — use a name that has never been authorized (this is the key condition; a server with cached tokens connects fine).
4. Settings → Customize → Connectors → Connect.
5. The UI shows "Connection to server failed. You can try again." No browser window opens. Logs in `~/Library/Logs/Claude-3p/main.log` show the sequence above.
Independent verification that the server is correct:
```sh
# 401 with an https resource_metadata pointer
curl -si -X POST https://metabase.example.com/api/metabase-mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
# discovery: resource matches the configured URL, all https, S256 + DCR present
curl -s https://metabase.example.com/.well-known/oauth-protected-resource/api/metabase-mcp
curl -s https://metabase.example.com/.well-known/oauth-authorization-server
```
---
## Claude Model
Not sure / Multiple models *(model-independent — this is in the MCP connect path)*
## Is this a regression?
Yes, this worked in a previous version
## Last Working Version
1.25927.0 (Claude Desktop)
## Claude Code Version
Claude **Desktop** 1.32352.1 — this is a Desktop MCP-host bug, not the CLI. Claude Code CLI on the same machine is 2.1.226.
## Platform
AWS Bedrock
## Operating System
macOS *(26.5.2, Apple Silicon)*
## Terminal/Shell
Other *(not applicable — Claude Desktop UI, not a terminal)*
---
## Additional Information
**A pre-registered OAuth client does not work around it.** Setting `oauth` (e.g. `{"clientId":"…","callbackPort":53280}`, the shape suggested by the app's own DCR error text) suppresses the anonymous fallback because the branch is gated on `!e.oauth`, but the error is still classified `transport`, so it parks for retry and still never opens the browser.
**Workaround that does work**, for anyone else blocked: supply a bearer token directly, which skips the OAuth path entirely. `pd()` returns false when an `Authorization` header is present, and the caller then takes the direct-connect branch (`if (!pd(e)) return ZE(e, n)`), so the probe's misclassification never comes into play:
```js
function pd(e){
return e.transport !== 'http' && e.transport !== 'sse' ? false
: e.oauth ? true
: !e.headersHelper && !Object.keys(e.headers ?? {}).some(k => k.toLowerCase() === 'authorization');
}
```
This trades per-user OAuth for a shared token, so it is a bridge rather than a fix.
**Scope caveat:** I verified this in third-party-inference mode with an org-plugin-delivered HTTP connector. Since `managedMcpServers` is also available in first-party scope and the failing code is in the shared `custom3p-mcp` host, I would expect any first-time OAuth connect through that host to be affected, but I have only confirmed the one configuration.
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Direzione di ricerca
Start by tracing the version-negotiation probe and connect-path classifier in the bundled custom3p-mcp host, especially YE, ZE, pd, and the hE/dE feature-flag entry point. Reproduce a first-time OAuth connection without cached tokens and compare the logs with the 1.25927.0 sequence. Done means the auth-required path reaches authorization and opens the browser instead of falling back to transport.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- javascript
- Ambito
- api, authentication, desktop
- Tipo di issue
- Bug
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Stato di attività
- Attiva
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 38/100