Description
When protected resource metadata contains a pathless resource URI, the TypeScript SDK normalizes it through new URL(...).href, which changes the resource indicator value by adding a trailing slash.
For example, protected resource metadata may contain:
{
"resource": "https://example.com",
"authorization_servers": ["https://login.microsoftonline.com/{tenant}/oauth2/v2.0"],
"scopes_supported": ["https://example.com/mcp:tools"]
}
The SDK then constructs OAuth requests with:
resource=https%3A%2F%2Fexample.com%2F
instead of:
resource=https%3A%2F%2Fexample.com
This changes the resource identifier from the value explicitly published in protected resource metadata.
Impact
This breaks OAuth with providers that require the resource parameter to exactly match the resource implied by the requested scopes. Microsoft Entra ID returns:
AADSTS9010010: The resource parameter provided in the request doesn't match with the requested scopes.
Removing the trailing slash makes the request work.
Expected behavior
If protected resource metadata explicitly provides "resource": "https://example.com", then the SDK should preserve that exact value when adding the OAuth resource parameter to both the authorization request and the token request.
Validation can still parse the value as a URL, but serialization should use the original metadata string rather than URL.href.
Actual behavior
The SDK serializes the resource via URL.href, which changes a pathless URI:
new URL("https://example.com").href
// "https://example.com/"
Likely cause
In the current SDK code, selectResourceURL() returns a URL object:
return new URL(resourceMetadata.resource);
Then the authorization and token request code serializes with .href:
authorizationUrl.searchParams.set("resource", resource.href);
tokenRequestParams.set("resource", resource.href);
This turns "https://example.com" into "https://example.com/".
Suggested fix
Preserve the resource metadata value as a string after validation. For example, return resourceMetadata.resource from selectResourceURL() and serialize with String(resource) rather than resource.href.
This may require changing the internal resource type from URL | undefined to string | URL | undefined, or introducing a small alias for OAuth resource indicators.
Related issue
This appears to be the SDK-level root cause of this Inspector issue:
modelcontextprotocol/inspector#927
Description
When protected resource metadata contains a pathless resource URI, the TypeScript SDK normalizes it through
new URL(...).href, which changes the resource indicator value by adding a trailing slash.For example, protected resource metadata may contain:
{ "resource": "https://example.com", "authorization_servers": ["https://login.microsoftonline.com/{tenant}/oauth2/v2.0"], "scopes_supported": ["https://example.com/mcp:tools"] }The SDK then constructs OAuth requests with:
instead of:
This changes the resource identifier from the value explicitly published in protected resource metadata.
Impact
This breaks OAuth with providers that require the
resourceparameter to exactly match the resource implied by the requested scopes. Microsoft Entra ID returns:Removing the trailing slash makes the request work.
Expected behavior
If protected resource metadata explicitly provides
"resource": "https://example.com", then the SDK should preserve that exact value when adding the OAuthresourceparameter to both the authorization request and the token request.Validation can still parse the value as a URL, but serialization should use the original metadata string rather than
URL.href.Actual behavior
The SDK serializes the resource via
URL.href, which changes a pathless URI:Likely cause
In the current SDK code,
selectResourceURL()returns aURLobject:Then the authorization and token request code serializes with
.href:This turns
"https://example.com"into"https://example.com/".Suggested fix
Preserve the resource metadata value as a string after validation. For example, return
resourceMetadata.resourcefromselectResourceURL()and serialize withString(resource)rather thanresource.href.This may require changing the internal
resourcetype fromURL | undefinedtostring | URL | undefined, or introducing a small alias for OAuth resource indicators.Related issue
This appears to be the SDK-level root cause of this Inspector issue:
modelcontextprotocol/inspector#927