Skip to content

fix: preserve exact resource URI from protected resource metadata - #1991

Open
Christian-Sidak wants to merge 1 commit into
modelcontextprotocol:mainfrom
Christian-Sidak:fix/issue-1968
Open

fix: preserve exact resource URI from protected resource metadata#1991
Christian-Sidak wants to merge 1 commit into
modelcontextprotocol:mainfrom
Christian-Sidak:fix/issue-1968

Conversation

@Christian-Sidak

Copy link
Copy Markdown
Contributor

Summary

  • selectResourceURL() was returning new URL(resourceMetadata.resource), which normalizes pathless URIs like https://example.com to https://example.com/ via URL.href
  • Both the authorization request and token request then serialized the resource with .href, further cementing the trailing slash
  • Changed selectResourceURL() to return the original metadata string directly (preserving the exact value), and updated all serialization to use String(resource) instead of resource.href
  • Updated the return type of selectResourceURL and the resource parameter in startAuthorization, executeTokenRequest, exchangeAuthorization, refreshAuthorization, and fetchToken from URL to URL | string to accommodate both metadata strings and custom validateResourceURL URL objects

Test plan

  • Existing 350 tests pass (updated one test expectation that was asserting the old incorrect normalized behavior)
  • Added new regression test: preserves pathless resource URI from PRM without adding trailing slash
  • Manually verified: new URL("https://example.com").href === "https://example.com/" (the bug), and that returning the string directly preserves "https://example.com"

Fixes #1968

…alization

When protected resource metadata contains a pathless URI like
"https://example.com", the SDK was normalizing it to "https://example.com/"
via URL.href. This broke OAuth with providers like Microsoft Entra ID that
require the resource parameter to exactly match the scopes' audience.

Change selectResourceURL to return the original metadata string directly
instead of wrapping it in new URL(), and update all serialization sites to
use String() instead of .href so URL objects are still handled correctly.

Fixes modelcontextprotocol#1968

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Christian-Sidak
Christian-Sidak requested a review from a team as a code owner April 30, 2026 08:27
@changeset-bot

changeset-bot Bot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 60993b2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@modelcontextprotocol/client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Apr 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@1991

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@1991

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@1991

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@1991

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@1991

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@1991

commit: 60993b2

@Christian-Sidak

Copy link
Copy Markdown
Contributor Author

Friendly bump -- let me know if anything needs changing.

1 similar comment
@Christian-Sidak

Copy link
Copy Markdown
Contributor Author

Friendly bump -- let me know if anything needs changing.

@Wesley-Miller

Copy link
Copy Markdown

Additional bump. Thanks

@Christian-Sidak

Copy link
Copy Markdown
Contributor Author

Friendly bump -- let me know if anything needs changing.

2 similar comments
@Christian-Sidak

Copy link
Copy Markdown
Contributor Author

Friendly bump -- let me know if anything needs changing.

@Christian-Sidak

Copy link
Copy Markdown
Contributor Author

Friendly bump -- let me know if anything needs changing.

@aarora79

aarora79 commented Aug 9, 2026

Copy link
Copy Markdown

Endorsing this from a production deployment. We run an MCP gateway/registry that fronts many MCP servers behind a single Microsoft Entra tenant (agentic-community/mcp-gateway-registry), and we hit exactly the #1968 bug: our PRM advertises a pathless resource (verified slash-free via curl), but the client sends resource=https://.../ on /authorize + /token, so Entra rejects the exchange with AADSTS9010010.

I reviewed the diff and it resolves our case correctly:

  • Returning resourceMetadata.resource verbatim from selectResourceURL (instead of new URL(...)) fixes it at the source — the URL.href normalization was the only thing adding the slash — and emitting String(resource) at both the /authorize and /token sites keeps it byte-for-byte.
  • A path-qualified resource (https://gw/<server>/mcp) is preserved unchanged, and the bare origin (https://gw) now goes out without the trailing slash Entra can't match.

One production detail that makes the pathless case matter beyond a single server: because a bare-origin resource is currently unusable on Entra, a gateway is forced to register one identifierUri per MCP server (Entra App ID URIs can't end in a slash and are matched by exact string), which runs into Entra's 999-URI-per-app cap and per-server operator toil. Emitting the origin verbatim collapses that back to a single origin registration for the whole gateway. So this fix removes real operational burden, not just a one-server annoyance.

The added regression test (pathless PRM resource -> no trailing slash) and the flipped assertion match the intended behavior. +1 to merging.

Related downstream reports for cross-reference: the same AADSTS9010010 surfaced via Claude Code in anthropics/claude-code#52871, and our tracking issue agentic-community/mcp-gateway-registry#990.

cc @omrishiv

@Christian-Sidak

Copy link
Copy Markdown
Contributor Author

Thanks @aarora79 -- great to have production confirmation. The single-origin-per-gateway use case (vs. one identifierUri per MCP server) is a clear illustration of why byte-for-byte preservation of the resource URI matters at scale. The cross-references to anthropics/claude-code#52871 and mcp-gateway-registry#990 are helpful context for maintainers.

@aarora79

Copy link
Copy Markdown

Hi @felixweinberger @pcarleton, would appreciate your attention on this one. selectResourceURL() returns new URL(resourceMetadata.resource), which normalizes a pathless https://example.com to https://example.com/ and breaks the exact-match resource parameter for Microsoft Entra ID (#1968, P2/bug/fix-proposed). Patch is 3 files, +75/−12, with a regression test and a changeset. Multiple people have confirmed it with Claude Code connecting to an MCP server via a Gateway with Entra ID being used as the IdP, please see anthropics/claude-code#52871.

@zoltan-fedor

Copy link
Copy Markdown

+1

2 similar comments
@olliekemp

Copy link
Copy Markdown

+1

@adrianB1996

Copy link
Copy Markdown

+1

@matthewcarlisle

matthewcarlisle commented Aug 14, 2026

Copy link
Copy Markdown

+1 - this is an actual production issue. We can't connect to Azure DevOps via MCP until this is fixed. Would love to use MCP for useful things.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OAuth resource indicator from protected resource metadata is normalized with a trailing slash

7 participants