[BUG] HTTP MCP server returning RFC 6750 401 without RFC 9728 `resource_metadata=` hint surfaces as 'Failed to connect' instead of triggering OAuth dance

Status Fixed / completed
Maintainer reply ✓ Yes — localden
Activity 5 comments · opened May 22, 2026 · closed May 27, 2026
💡 Likely answer: A maintainer (localden, collaborator) responded on this thread — see the highlighted reply below.

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

View original on GitHub ↗

5 Comments

new-horizons-pioneer · 4 months ago

Server-side update: I shipped a corresponding backend fix on our SpeakUp MCP AS that adds the RFC 9728 resource_metadata= parameter to the WWW-Authenticate header on every 401 from /v2/mcp/transport/. Live as of 2026-05-22 06:16Z on api.speak-up.pro.

Updated server response:

$ 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":"t","version":"1"}}}' | grep -iE 'HTTP/|www-authenticate'
HTTP/2 401
www-authenticate: Bearer error="invalid_token", resource_metadata="https://api.speak-up.pro/.well-known/oauth-protected-resource"

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-resource is also live (200 OK, full document with authorization_servers, scopes_supported, etc.). With ~/.claude.json speakup-prod set to {"type": "http", "url": "https://api.speak-up.pro/v2/mcp/transport/", "headers": {}}, claude mcp list should now flip from ✗ Failed to connect to ! Needs authentication once the CLI honours either the resource_metadata= hint OR the apex /.well-known/oauth-authorization-server fallback. Today it still shows Failed 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 in src/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.

new-horizons-pioneer · 4 months ago

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 rejecting null on optional fields like service_documentation.

[ERROR] MCP server "speakup-prod" ZodError: [
  {
    "expected": "string",
    "code": "invalid_type",
    "path": [ "service_documentation" ],
    "message": "Invalid input: expected string, received null"
  }
]

Fix shipped via https://github.com/TheSpeakUp/speak_up_backend/pull/710 (squash e5924cc4, deployed 2026-05-22 06:35Z) — response_model_exclude_none=True on all 7 well-known metadata routes. Post-deploy curl -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:

2026-05-22T06:36:02.293Z [DEBUG] MCP server "speakup-prod": Skipping connection (cached needs-auth)

= no more ZodError, server is now correctly classified as needs-auth.

Remaining cosmetic gap on the client side: claude mcp list still surfaces ✗ Failed to connect for speakup-prod even though the debug log says cached needs-auth. The real OAuth dance is presumably gated on an actual tool call from a chat session, not the headless probe — so mcp list lies. If the intent is for mcp list to also flip to ! Needs authentication once a server is correctly classified as needing auth (the way other built-in connectors do), that branching is the only thing keeping speakup-prod from 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.

jshaofa-ui · 4 months ago

Solution: MCP OAuth 401 Without resource_metadata Hint

Root Cause

Claude Code's MCP HTTP transport requires the RFC 9728 resource_metadata WWW-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_metadata is absent, derive /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server from 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 mcpServers config.

4. Enhanced Error Messages - Actionable guidance when no OAuth metadata can be discovered.

Testing

  • Unit: parseBearerChallenge with/without resource_metadata
  • Integration: RFC 6750-only server + RFC 9728 server
  • Regression: Existing RFC 9728 servers still work

Impact

  • Severity: High - custom MCP servers without RFC 9728 hints broken
  • Fix Risk: Low - adds fallback path
  • Backward Compatible: Yes

Full solution: solutions/claude-code-61376-mcp-oauth-401-resource-metadata-hint-fix.md

localden collaborator · 4 months ago

Thanks for the report and the server-side debugging notes. This is fixed and will ship in the next release.

A ZodError during OAuth discovery (e.g. service_documentation: null or other optional-field schema mismatches) is now classified as needs-auth rather than failed, so the authentication prompt surfaces correctly instead of the connector silently disappearing.

github-actions[bot] · 2 months ago

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.