Repository navigation
Description
What happened?
Question
Is it intended that the client accepts Protected Resource Metadata (PRM) whose resource is a parent path of the server URL? And if so, how should that be reconciled with RFC 9728 §3.3?
Current behaviour
selectResourceURL validates resourceMetadata.resource with checkResourceAllowed, which accepts any same-origin resource whose path is a prefix of the server URL's path (authUtils.ts). The less specific URL is then returned and sent as the RFC 8707 resource parameter.
Example:
- Server URL:
https://host/mcp/gateway/bundle/my-bundle 401withWWW-Authenticate: Bearer resource_metadata="https://host/mcp/gateway/.well-known/oauth-protected-resource/bundle/my-bundle"- PRM:
{"resource": "https://host/mcp/gateway", ...}
The SDK accepts this and requests a token for https://host/mcp/gateway. VS Code rejects the same metadata (oauth.ts: "Per RFC 9728, these MUST match"), so a server that works with SDK-based clients fails in VS Code.
Why I'm asking
RFC 9728 §3.3:
If the protected resource metadata was retrieved from a URL returned by the protected resource via the WWW-Authenticate resource_metadata parameter, then the resource value returned MUST be identical to the URL that the client used to make the request to the resource server. If these values are not identical, the data contained in the response MUST NOT be used.
§7.3 gives impersonation as the reason for the identical-match rule. The MCP spec adopts RFC 9728 and asks clients to "provide the most specific URI that they can" (2025-11-25, Canonical Server URI).
The prefix matching was introduced in #638. Its description says resource URIs are used as-is "to allow having different MCP servers under different subpaths … w/o spilling their resource authorization to each other". It also warns that a non-specific resource lets a "bad apple" server benefit from resource confusion with its siblings. Accepting a parent-path resource from the PRM seems to allow exactly that: a server at /a/b can get the client to request a token for /a. I couldn't find a rationale for the prefix rule in the PR or its review.
#2751 already notes in passing that this leniency hides non-compliant server setups.
#3041 covers the trailing-slash asymmetry in the same function. Neither asks whether the prefix rule itself is intended.
Questions
- Is accepting a parent-path
resourceon the client side intentional? - If yes, is it meant to apply to metadata discovered via
WWW-Authenticate, where §3.3 requires identity? Or only to the root well-known fallback? - If no, would you accept a change that makes
selectResourceURLrequire equality? This would be normalized equality, tolerant of a trailing slash as in checkResourceAllowed rejects a protected resource that differs from the server URL only by a trailing slash (one direction only) #3041, possibly with an opt-in for lenient matching to avoid breaking existing servers.
Note: downstream clients reuse checkResourceAllowed in their own validateResourceURL hooks (e.g. OpenCode), so the answer also determines what they should do.
What did you expect?
No response
Code to reproduce
SDK version
No response
Area
Auth
Activity
arhancanli commentedon Oct 23, 2026
I read
checkResourceAllowedinauthUtils.ts(didn't run anything). The comment on it describes a prefix check on path segments, and the/apivs/api123guard is correct for that purpose. So the helper does what it says; the open question is whetherselectResourceURLshould be using a prefix check for PRM at all.On your question 2, my reading of §3.3 is that the identity rule isn't limited to the
WWW-Authenticatepath. The section has two cases: when the document came fromresource_metadata,resourcemust equal the URL the client requested; when it came from the well-known URL,resourcemust equal the identifier the well-known suffix was inserted into. Both end with "MUST NOT be used" if they differ. If that reading holds, a parent-pathresourceis invalid in both discovery routes, and a "lenient for root fallback only" carve-out would not be spec-backed either.If the maintainers want to avoid breaking existing servers, I'd make the lenient mode opt-in and say in the changelog which previously accepted setups stop working, rather than keep it as the default. The #638 sibling-isolation reasoning actually argues for strict: with equality, a server at
/a/bcan never obtain a token audience of/a.One edge for the equality version: normalization.
new URL()leaveshttps://hostandhttps://host/as the samepathname(/), but/mcpand/mcp/stay different, so #3041's tolerance should be limited to a single trailing slash and not collapse repeated slashes or dot segments.Do you know whether any of the conformance servers publish a PRM whose
resourceis a parent path, or is VS Code's rejection the only case reported so far?Ben-Home commentedon Oct 28, 2026
arhancanli commentedon Oct 28, 2026
Splitting the two paths is the right shape. One thing I would check in the second half before agreeing that the well-known case can be tolerant: as I read RFC 9728 section 3.3, the well-known route has its own identity rule. The returned
resource"MUST be identical" to the resource identifier into which the well-known suffix was inserted to build the URL the client fetched, and a mismatch means the data MUST NOT be used.That rule is satisfiable without prefix matching. When the client probes the path-aware URL first and then the root URL, each probe has a different identifier, so the comparison target is the identifier used for that probe, not the server URL the client started from. A root document carrying the root
resourcepasses strict equality for the root probe, and the SDK can still pick the most specific document it found. What stays rejected is a document fetched at the path-aware URL that claims the parent. That is the spill case you describe.So the helper may only need one argument changed:
resourceMatches(expectedIdentifier, resource), withexpectedIdentifierbeing the URL the client used to call the server on the challenge route, and the probed identifier on the fallback route. Does your multi-tenant gateway serve a root-level PRM too, or only per-tenant ones? That decides whether the root probe ever returns something a client has to accept.fix(client): reject ambiguous PRM resource URLs (modelcontextprotocol…
bbdu3 commentedon Nov 10, 2026
Hi all. My understanding is that
checkResourceAllowedimplements its documented path-containment check, but that check is insufficient for validating PRM identity under RFC 9728 §3.3.I agree with the concern about accepting a parent-path
resource: it can cause the client to request a different resource audience and hide server misconfiguration. I also agree with keeping the shared helper unchanged, since downstream consumers may depend on its existing semantics.The distinction in @arhancanli’s last comment is the approach I favor: both discovery routes require identity validation, but the expected identifier depends on the discovery attempt:
WWW-Authenticatechallenge, compare against the URL used to request the protected resource.This preserves legitimate root fallback without allowing arbitrary parent-path claims. I don’t think §3.3 supports retaining prefix tolerance for well-known discovery generally. I would also keep general trailing-path-slash tolerance separate from this issue.
I’ve opened a PR implementing this approach. It:
checkResourceAllowedunchanged. Custom resource selection remains available after PRM validation.@PhysicallyActive, does this interpretation match the issue you’re raising and your deployment’s discovery behavior? I’d appreciate your review, particularly of the root-fallback handling and compatibility choices. Feedback from the other participants and maintainers would also be welcome.
If I’ve misunderstood the intended behavior or taken the wrong implementation approach, I’m happy to revise the PR—or close it if this isn’t the right direction.
PhysicallyActive commentedon Nov 10, 2026
@bbdu3 Thank you for the PR. Your interpretation agrees with my issue.
Our deployment uses the 401 challenge for discovery. Each bundle has its own metadata URL in the
resource_metadataparameter. We do not publish metadata at the root well-known URL or at the path-inserted well-known URL.The root well-known URL on our host gives a
200status with an HTML page from a different application. Thus, the root fallback must not use a response that is not JSON.Before, our metadata used the parent path as
resourcefor all bundles. OpenCode accepted this value, but VS Code did not. Now each bundle uses its own URL asresource, and the two clients connect correctly.I will examine the root-fallback handling in your PR.
niksa90 commentedon Nov 11, 2026
Not the only one, and probably not rare. Some numbers for the changelog the strict version would need.
I run toolmenu, which tests MCP servers from the client's side. Its
auth checkwalks the discovery a client does, without logging in. On 2026-11-09 it ran against 109 big-vendor endpoints and a sample of 2,000 hosted servers from the registry. 47 serve protected resource metadata whoseresourceis a same-origin parent of the server URL:resource_metadataURL from the 401 names a document claiming the parent. Under per-route equality, all 46 stop working. 8 are vendor endpoints, for examplehttps://mcp.semgrep.ai/mcp→"https://mcp.semgrep.ai/"andhttps://developer.visa.com/mcp→"https://developer.visa.com".https://api.wistia.com/mcp/apiclaims"https://api.wistia.com".Nearly all are the same shape: an
/mcpendpoint on a host that names the bare origin. Which matches the fix described above: each server names its own URL, and both clients connect. So the break is real but cheap to fix on the server side, which is a fair argument for strict-with-opt-in rather than lenient-by-default.On #3041 and normalization: 20 more differ only by a trailing slash. 16 of those have no path (
https://mcp.box.com→"https://mcp.box.com/"), whichnew URL()already treats as equal. The other 4 have a path (https://mcp.newrelic.com/mcp→"https://mcp.newrelic.com/mcp/"), so the single-trailing-slash tolerance decides whether those 4 keep working.Every URL is in
findings.jsonl(ruleauth/resource-metadata). One run, no login: it shows what the metadata says, not what an authorization server does with a token for the parent.AI disclosure: Claude Code helped pull these numbers from the run's data.
bbdu3 commentedon Nov 13, 2026
@niksa90. Thanks for sharing the scan and the underlying data. I checked the findings, and they confirm that the compatibility concern is real: multiple deployed servers advertise a parent resource through the challenge route. This goes beyond the original VS Code example.
My PR follows the per-discovery identity interpretation of RFC 9728 §3.3: validate against the requested URL for a challenge, or against the identifier used for the particular well-known probe. That preserves legitimate root fallback, but it also rejects metadata that the SDK currently accepts. Your results make the practical impact of that change much clearer.
The tension is between enforcing the specified identity boundary and preserving interoperability with existing deployments. The scan establishes the metadata mismatch, though it does not establish whether authorization succeeds today or how the authorization server handles that audience. Similarly, changing the advertised resource may require coordinated changes to audience configuration and token validation, so I would be cautious about assuming every server-side fix is trivial.
I still lean toward strict validation by default, but I think maintainers should review the policy before we settle on an implementation:
/mcpversus/mcp/, and keeping discovery construction consistent with identity validation.I’ll close the PR for now until the expected behavior is clarified by the maintainers. I’m happy to revise or reopen it once maintainers have agreed on the intended behavior. Thanks again for providing concrete deployment evidence—it helps make this a much better-informed decision.