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
Summary
A user-added remote HTTP MCP server that responds to unauthenticated
initializewith a standard401 Bearer error="invalid_token"— but without an RFC 9728resource_metadata=parameter inWWW-Authenticate— surfaces as✗ Failed to connectinclaude mcp listinstead 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 usableresource_metadata=hint, the client should fall back to probing the resource origin's/.well-known/oauth-authorization-serverper 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_uriparameter).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:
All four endpoints are reachable; DCR + PKCE + authorization_code grant verified via raw curl.
Step 2 — server 401 response on unauthenticated
initialize:RFC 6750 §3 compliant (
error="invalid_token"), but noresource_metadata=parameter.Step 3 —
~/.claude.jsonentry:{ "mcpServers": { "speakup-prod": { "type": "http", "url": "https://api.speak-up.pro/v2/mcp/transport/", "headers": {} } } }Step 4 —
claude mcp listoutput: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.jsonheaders, 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 anAuthorization: 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 parsableresource_metadata=hint, fall back to probing<origin>/.well-known/oauth-authorization-serverper RFC 8414 §3 before declaring the connection failed. If that returns valid AS metadata withregistration_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
P0tTlVr…) signed tokens; nginx frontingImpact
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
authorization_uri