Skip to content

[v2] Client accepts a parent-path protected resource metadata, is this intended given RFC 9728? #3077

Open

Description

@PhysicallyActive

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
  • 401 with WWW-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

  1. Is accepting a parent-path resource on the client side intentional?
  2. 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?
  3. If no, would you accept a change that makes selectResourceURL require 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

  1. added
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    on Oct 23, 2026
  2. arhancanli commented on Oct 23, 2026

    @arhancanli

    I read checkResourceAllowed in authUtils.ts (didn't run anything). The comment on it describes a prefix check on path segments, and the /api vs /api123 guard is correct for that purpose. So the helper does what it says; the open question is whether selectResourceURL should 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-Authenticate path. The section has two cases: when the document came from resource_metadata, resource must equal the URL the client requested; when it came from the well-known URL, resource must 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-path resource is 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/b can never obtain a token audience of /a.

    One edge for the equality version: normalization. new URL() leaves https://host and https://host/ as the same pathname (/), but /mcp and /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 resource is a parent path, or is VS Code's rejection the only case reported so far?

  3. Ben-Home commented on Oct 28, 2026

    @Ben-Home
  4. arhancanli commented on Oct 28, 2026

    @arhancanli

    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 resource passes 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), with expectedIdentifier being 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.

  5. bbdu3 commented on Nov 10, 2026

    @bbdu3

    Hi all. My understanding is that checkResourceAllowed implements 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:

    • For a WWW-Authenticate challenge, compare against the URL used to request the protected resource.
    • For path-specific well-known discovery, compare against the identifier used to construct that discovery URL.
    • For root well-known fallback, compare against the root identifier.

    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:

    • Validates PRM identity before using its authorization-server information.
    • Revalidates cached metadata and records root-fallback provenance; older root-resource caches without that information are rediscovered.
    • Leaves checkResourceAllowed unchanged. Custom resource selection remains available after PRM validation.
    • Preserves trailing slashes when constructing path-specific PRM discovery URLs and rejects ambiguous URL parsing, including backslash authority boundaries.
    • Documents compatibility changes and preserves valid metadata resource strings in outgoing OAuth requests.

    @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.

  6. PhysicallyActive commented on Nov 10, 2026

    @PhysicallyActive
    Author

    @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_metadata parameter. 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 200 status 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 resource for all bundles. OpenCode accepted this value, but VS Code did not. Now each bundle uses its own URL as resource, and the two clients connect correctly.

    I will examine the root-fallback handling in your PR.

  7. niksa90 commented on Nov 11, 2026

    @niksa90

    is VS Code's rejection the only case reported so far?

    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 check walks 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 whose resource is a same-origin parent of the server URL:

    • 46 on the challenge route: the resource_metadata URL from the 401 names a document claiming the parent. Under per-route equality, all 46 stop working. 8 are vendor endpoints, for example https://mcp.semgrep.ai/mcp → "https://mcp.semgrep.ai/" and https://developer.visa.com/mcp → "https://developer.visa.com".
    • 1 on the fallback route, and it's the spill case above: the path-aware well-known document for https://api.wistia.com/mcp/api claims "https://api.wistia.com".

    Nearly all are the same shape: an /mcp endpoint 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/"), which new 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 (rule auth/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.

  8. bbdu3 commented on Nov 13, 2026

    @bbdu3

    @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:

    • Whether strict validation should become the default, and how to communicate and roll out that compatibility change.
    • Whether an explicit compatibility option is warranted, and how narrowly it should be scoped.
    • How to handle trailing-slash normalization, distinguishing an empty root path from /mcp versus /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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions