[BUG] RFC 9728 protected-resource check compares against literal "http" instead of configured URL for type:http MCP servers
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.jsonentry{"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
- Register an HTTP-type remote MCP server whose upstream serves
/.well-known/oauth-protected-resourcemetadata with aresourcematching the configured URL exactly:
``json``
{ "mcpServers": { "example": { "type": "http", "url": "https://your-server.example.com/mcp" } } }
- Run
/mcp, select the server, choose Authenticate. - Complete the browser OAuth consent screen.
- Observe the failure:
SDK auth failed: Protected resource <real-url> does not match expected http (or origin). - 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()/serverUrltype-confusion described here, since our two URLs are already identical. - #96847 (open) — wrong
resourcevalue sent at the token-exchange step on first auth attempt; different code path (post-consent) from this pre-flight metadata-comparison bug.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗