[BUG] HTTP MCP server returning RFC 6750 401 without RFC 9728 `resource_metadata=` hint surfaces as 'Failed to connect' instead of triggering OAuth dance
Summary
A user-added remote HTTP MCP server that responds to unauthenticated initialize with a standard 401 Bearer error="invalid_token" — but without an RFC 9728 resource_metadata= parameter in WWW-Authenticate — surfaces as ✗ Failed to connect in claude mcp list instead of triggering the OAuth 2.1 dance. This blocks first-call OAuth discovery for any hosted MCP whose backend ships RFC 8414 /.well-known/oauth-authorization-server (with full DCR + authorize + token + revoke endpoints) but does not yet ship the RFC 9728 Protected Resource Metadata document.
The expected behavior: when the 401 carries WWW-Authenticate: Bearer … and no usable resource_metadata= hint, the client should fall back to probing the resource origin's /.well-known/oauth-authorization-server per RFC 8414 §3 and proceed with DCR. Cursor's MCP client reportedly handles this case; Claude Code does not as of v2.1.x.
This is related to but distinct from #59463 (built-in Figma connector, similar symptom but tied to a non-standard authorization_uri parameter).
Reproduction
Hosted MCP server: https://api.speak-up.pro/v2/mcp/transport/ (SpeakUp production AS, fully RFC 8414 compliant).
Step 1 — confirm AS metadata is live:
$ curl -sS https://api.speak-up.pro/.well-known/oauth-authorization-server | \
jq -r '.registration_endpoint, .authorization_endpoint, .token_endpoint, .revocation_endpoint'
https://api.speak-up.pro/v2/mcp/oauth/register
https://api.speak-up.pro/v2/mcp/oauth/authorize
https://api.speak-up.pro/v2/mcp/oauth/token
https://api.speak-up.pro/v2/mcp/oauth/revoke
All four endpoints are reachable; DCR + PKCE + authorization_code grant verified via raw curl.
Step 2 — server 401 response on unauthenticated initialize:
$ curl -sS -i -X POST https://api.speak-up.pro/v2/mcp/transport/ \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize", \
"params":{"protocolVersion":"2024-11-05","capabilities":{}, \
"clientInfo":{"name":"test","version":"1.0"}}}'
HTTP/2 401
content-type: application/json
www-authenticate: Bearer error="invalid_token"
{"message_key":"mcp.hosted_transport.missing_token","message":"MCP bearer token is required"}
RFC 6750 §3 compliant (error="invalid_token"), but no resource_metadata= parameter.
Step 3 — ~/.claude.json entry:
{
"mcpServers": {
"speakup-prod": {
"type": "http",
"url": "https://api.speak-up.pro/v2/mcp/transport/",
"headers": {}
}
}
}
Step 4 — claude mcp list output:
speakup-prod: https://api.speak-up.pro/v2/mcp/transport/ (HTTP) - ✗ Failed to connect
Expected: ! Needs authentication, plus a browser-side OAuth dance triggered on the next tool call.
Workaround (manual full curl dance)
For the issue reporter's own session this means scripting DCR → /authorize → /consent → /token via curl and pre-baking the resulting access_token into ~/.claude.json headers, which obviously defeats the point of supporting OAuth-authenticated remote MCP servers. ES256 access tokens issued through that manual dance work fine with the existing MCP HTTP transport once an Authorization: Bearer … header is present, so the gap is purely in client-side discovery.
Suggested fix
When a remote HTTP MCP server returns 401 with WWW-Authenticate: Bearer … and no parsable resource_metadata= hint, fall back to probing <origin>/.well-known/oauth-authorization-server per RFC 8414 §3 before declaring the connection failed. If that returns valid AS metadata with registration_endpoint, proceed with DCR. Today the client treats this case as a hard failure and surfaces only ✗ Failed to connect.
Belt-and-braces: if the resource is also missing RFC 9728 PRM and the well-known AS metadata is absent, it is reasonable to fall back to ✗ Failed to connect. But strict requirement of PRM blocks every backend that has not yet implemented RFC 9728 — which today includes anyone shipping MCP 2025-06-18 spec compliance without the optional RFC 9728 layer on top.
Environment
- Claude Code CLI v2.1.x (Opus 4.7, 1M context, macOS 15 Darwin 25.4.0)
- Server: FastAPI backend implementing full Phase 6 MCP OAuth (DCR + /authorize + /token + /revoke) on top of MCP 2025-06-18 hosted HTTP transport; ES256 (kid
P0tTlVr…) signed tokens; nginx fronting
Impact
Blocks the entire "external client signs into our hosted MCP via standard OAuth dance" rollout. Today the only path that works through Claude Code is pre-baking a bootstrap token from an out-of-band admin flow — fine for first-party operators, but defeats the whole purpose of shipping RFC-compliant OAuth on the server side.
Related
- #59463 — built-in Figma connector "Failed to connect" with non-standard
authorization_uri - #44830 — RFC 9728 discovery poisoning
- #26675 — pre-configured OAuth client credentials without DCR
5 Comments
Server-side update: I shipped a corresponding backend fix on our SpeakUp MCP AS that adds the RFC 9728
resource_metadata=parameter to theWWW-Authenticateheader on every 401 from/v2/mcp/transport/. Live as of 2026-05-22 06:16Z onapi.speak-up.pro.Updated server response:
So the bug repro is now even cleaner: the server emits the RFC 9728 discovery breadcrumb the client needs, and the linked PRM document at
https://api.speak-up.pro/.well-known/oauth-protected-resourceis also live (200 OK, full document withauthorization_servers,scopes_supported, etc.). With~/.claude.jsonspeakup-prodset to{"type": "http", "url": "https://api.speak-up.pro/v2/mcp/transport/", "headers": {}},claude mcp listshould now flip from✗ Failed to connectto! Needs authenticationonce the CLI honours either theresource_metadata=hint OR the apex/.well-known/oauth-authorization-serverfallback. Today it still showsFailed to connect, confirming the gap is entirely client-side.Backend PR for reference (squash
4823241c): https://github.com/TheSpeakUp/speak_up_backend/pull/708 — diff is two helpers + two call-site swaps insrc/MCP/hosted_runtime.py, plus three test assertions. Trivial change on the server, but the CLI still needs the discovery fallback to unlock the OAuth dance for any backend that ships RFC 6750 + RFC 8414 + RFC 9728.Second server-side fix shipped: the
resource_metadata=hint from PR #708 unlocked discovery, but my own debug log against the freshly deployed AS then surfaced a deeper bug — strict Zod parsing rejectingnullon optional fields likeservice_documentation.Fix shipped via https://github.com/TheSpeakUp/speak_up_backend/pull/710 (squash
e5924cc4, deployed 2026-05-22 06:35Z) —response_model_exclude_none=Trueon all 7 well-known metadata routes. Post-deploycurl -sS https://api.speak-up.pro/.well-known/oauth-authorization-server | jq 'with_entries(select(.value == null))'returns{}(was{"service_documentation": null}before).Claude Code debug log after both fixes:
= no more ZodError, server is now correctly classified as needs-auth.
Remaining cosmetic gap on the client side:
claude mcp liststill surfaces✗ Failed to connectforspeakup-prodeven though the debug log sayscached needs-auth. The real OAuth dance is presumably gated on an actual tool call from a chat session, not the headless probe — somcp listlies. If the intent is formcp listto also flip to! Needs authenticationonce a server is correctly classified as needing auth (the way other built-in connectors do), that branching is the only thing keepingspeakup-prodfrom looking fully wired in the CLI. The user-facing flow inside a real chat session should now actually trigger the browser dance — I'll confirm that next.Solution: MCP OAuth 401 Without
resource_metadataHintRoot Cause
Claude Code's MCP HTTP transport requires the RFC 9728
resource_metadataWWW-Authenticate parameter to initiate OAuth, but many servers only send RFC 6750-compliant 401 responses without this hint. The code treats an OPTIONAL parameter as mandatory.Proposed Fix
1. Fallback Well-Known URI Discovery - When
resource_metadatais absent, derive/.well-known/oauth-protected-resourceand/.well-known/oauth-authorization-serverfrom the server origin.2. OAuth Flow Initiation - Support Authorization Code (PKCE) and Device Authorization flows with discovered metadata.
3. Manual OAuth Config Override - Allow users to configure OAuth parameters in
mcpServersconfig.4. Enhanced Error Messages - Actionable guidance when no OAuth metadata can be discovered.
Testing
parseBearerChallengewith/withoutresource_metadataImpact
Full solution:
solutions/claude-code-61376-mcp-oauth-401-resource-metadata-hint-fix.mdThanks for the report and the server-side debugging notes. This is fixed and will ship in the next release.
A
ZodErrorduring OAuth discovery (e.g.service_documentation: nullor other optional-field schema mismatches) is now classified asneeds-authrather thanfailed, so the authentication prompt surfaces correctly instead of the connector silently disappearing.This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.