Skip to content

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

@new-horizons-pioneer

Description

@new-horizons-pioneer

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions