[BUG] RFC 9728 protected-resource check compares against literal "http" instead of configured URL for type:http MCP servers

Status Open
Reported on v2.1.274
Maintainer reply None cached
Activity 1 comment · opened Sep 26, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet (closest are #58735, a real domain-mismatch case that's closed/stale, and #96847, which is about the token-exchange step rather than this pre-flight metadata check)
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

Environment:

  • Claude Code 2.1.274 (Homebrew cask; confirmed latest available via brew outdated)
  • macOS (Darwin 25.6.0)
  • Server: an internal FastMCP-based server, .mcp.json entry {"type": "http", "url": "https://server.axway.org/mcp"}

Summary:

Authenticating against an HTTP-type MCP server whose OAuth protected-resource metadata (/.well-known/oauth-protected-resource/...) declares a resource matching the exact URL configured in .mcp.json still fails with:

SDK auth failed: Protected resource https://server.axway.org/mcp does not match expected http (or origin)

Note the "expected" side: it's the literal 4-character string http, not a URL. This isn't a real domain mismatch (unlike #58735, where both sides of the error are real, different URLs) — the comparison value itself is wrong.

I pulled the compiled CLI apart (strings on the cask binary, 2.1.274) to find the actual comparison code:

async function ms(e,{serverUrl:t, authorizationCode:r, iss:s, scope:a, resourceMetadataUrl:n, fetchFn:i, skipIssuerMetadataValidation:l, forceReauthorization:c}){
  ...
  let g = await Mc(t, e, _);   // t = serverUrl, e = provider, _ = fetched resourceMetadata
  ...
}

async function Mc(e,t,r){
  let s = Fs(e);   // e here is `t` (serverUrl) from the call site
  if(t.validateResourceURL) return await t.validateResourceURL(s, r?.resource);
  if(!r) return;
  if(!Hs({requestedResource:s, configuredResource:r.resource}))
    throw Error(`Protected resource ${r.resource} does not match expected ${s} (or origin)`);
  return new URL(r.resource)
}

s = Fs(serverUrl) is the "expected" value in the thrown error. For it to render as the literal string http, Fs() must be receiving something other than a bare URL string — most likely the whole MCP server config object ({type: "http", url: "..."}) is reaching this call site instead of just its .url, and Fs() falls back to a .type-like field when given a non-string/non-URL input.

Practical effect: any type: "http" MCP server whose upstream now serves RFC 9728 protected-resource metadata can never authenticate, regardless of whether the declared resource actually matches the configured server URL — because the "expected" side of the comparison is never a real URL to begin with.

What Should Happen?

Fs() / the value passed as serverUrl into this resource-binding check should always be the configured server's URL string, so the RFC 9728 comparison (added in v2.1.85, per #58735) either passes when resource genuinely matches, or reports the two real URLs being compared when it doesn't — never a bare scheme/type string.

Error Messages/Logs

SDK auth failed: Protected resource https://server.axway.org/mcp does not match expected http (or origin)

Also reproduced via the /plugin status panel:

Our MCP Server
   Status:           needs authentication
   Auth:             not authenticated
   URL:              http
   Config location:  /Users/ingo/.claude.json
   SDK auth failed: Protected resource https://server.axway.org/mcp does not match expected http (or origin)

Note the URL: http line in the status panel itself — the same literal type value is leaking into a second place, the panel's URL display, not just the error message. That's consistent with the config object (not .url) being passed around rather than a plain string somewhere in this code path.

Steps to Reproduce

  1. Register an HTTP-type remote MCP server whose upstream serves /.well-known/oauth-protected-resource metadata with a resource matching the configured URL exactly:

``json
{ "mcpServers": { "example": { "type": "http", "url": "https://your-server.example.com/mcp" } } }
``

  1. Run /mcp, select the server, choose Authenticate.
  2. Complete the browser OAuth consent screen.
  3. Observe the failure: SDK auth failed: Protected resource <real-url> does not match expected http (or origin).
  4. Confirm server-side correctness independently: curl https://your-server.example.com/.well-known/oauth-protected-resource/<path> returns {"resource": "<same real-url>", ...} — i.e. the two sides genuinely match, yet the client still rejects.

Claude Model

N/A (this is a CLI/SDK auth bug, not model-related)

Is this a regression?

Yes, relative to any Claude Code version before v2.1.85, when this RFC 9728 resource-binding check didn't exist yet (per #58735's investigation) — MCP OAuth to this same server worked prior to that. Whether the Fs()/serverUrl mixup itself is a regression from a more recent change, or has existed since the check was introduced, I can't tell from the binary alone.

Last Working Version

Unknown — predates v2.1.85 per the above; haven't bisected further.

Claude Code Version

2.1.274

Platform

AWS Bedrock

Operating System

macOS (Darwin 25.6.0)

Terminal/Shell

zsh

Additional Information

Related but distinct existing issues:

  • #58735 (closed/stale) — a genuine domain-mismatch case (two different real URLs); this issue's fix likely doesn't cover the Fs()/serverUrl type-confusion described here, since our two URLs are already identical.
  • #96847 (open) — wrong resource value sent at the token-exchange step on first auth attempt; different code path (post-consent) from this pre-flight metadata-comparison bug.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗