[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

Status Open
Reported on v2.1.226
Maintainer reply None cached
Activity 0 comments · opened Aug 18, 2026

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: <server>: 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:

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:

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:

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 transporttransport-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: <server>, clientId: … }
[custom3p-mcp] needs authorization — parked for renderer { name: <server> }
[custom3p-mcp] opening authorize URL { server: <server>, host: …, state: … }
[custom3p-mcp] waiting for OAuth callback (browser opened) { server: <server> }
[custom3p-mcp] saved OAuth tokens { server: <server>, hasRefresh: true, ttlSec: 3600 }
[custom3p-mcp] server connected { name: <server>, 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: <typed>} 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: '<server>', clientId: '<redacted>' }
2026-08-18 08:01:03 [info] [custom3p-mcp] default OAuth probe failed — falling back to open spawn { name: '<server>', error: 'Version negotiation probe failed: <server>: OAuth needs interactive authorization (no cached tokens)' }
2026-08-18 08:01:04 [error] [custom3p-mcp] connect failed — parked for retry { name: '<server>', 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: <server> — reconnecting (http)
2026-08-18 08:01:17 [info] [custom3p-mcp] default OAuth probe failed — falling back to open spawn { name: '<server>', error: 'Version negotiation probe failed: <server>: OAuth needs interactive authorization (no cached tokens)' }
2026-08-18 08:01:18 [error] LocalAgentModeSessions.authorizeDirectMcpServer: <server> — 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: '<server>', error: 'Version negotiation probe failed: <server>: OAuth needs interactive authorization (no cached tokens)' }
2026-08-18 08:29:03 [error] [custom3p-mcp] connect failed — parked for retry { name: '<server>', 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.

  1. 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).
  2. Settings → Customize → Connectors → Connect.
  3. 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:

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

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.

View original on GitHub ↗