Skip to content

Commit ff57462

Browse files
committed
feat: add validate_resource_url callback to OAuthClientProvider
Allows clients to override or disable PRM resource validation. Called with (server_url, prm_resource) and can raise to reject, return to accept, or implement custom logic. When not provided, default behavior validates per RFC 8707 and rejects mismatches.
1 parent 297eded commit ff57462

File tree

1 file changed

+16
-4
lines changed

1 file changed

+16
-4
lines changed

src/mcp/client/auth/oauth2.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ def __init__(
229229
callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None = None,
230230
timeout: float = 300.0,
231231
client_metadata_url: str | None = None,
232+
validate_resource_url: Callable[[str, str | None], Awaitable[str | None]] | None = None,
232233
):
233234
"""Initialize OAuth2 authentication.
234235
@@ -243,6 +244,11 @@ def __init__(
243244
advertises client_id_metadata_document_supported=true, this URL will be
244245
used as the client_id instead of performing dynamic client registration.
245246
Must be a valid HTTPS URL with a non-root pathname.
247+
validate_resource_url: Optional callback to override resource URL validation.
248+
Called with (server_url, prm_resource) where prm_resource is the resource
249+
from Protected Resource Metadata (or None if not present). Must return the
250+
resource URL to use, or None to omit it. If not provided, default validation
251+
rejects mismatched resources per RFC 8707.
246252
247253
Raises:
248254
ValueError: If client_metadata_url is provided but not a valid HTTPS URL
@@ -263,6 +269,7 @@ def __init__(
263269
timeout=timeout,
264270
client_metadata_url=client_metadata_url,
265271
)
272+
self._validate_resource_url_callback = validate_resource_url
266273
self._initialized = False
267274

268275
async def _handle_protected_resource_response(self, response: httpx.Response) -> bool:
@@ -476,12 +483,17 @@ async def _handle_oauth_metadata_response(self, response: httpx.Response) -> Non
476483
metadata = OAuthMetadata.model_validate_json(content)
477484
self.context.oauth_metadata = metadata
478485

479-
def _validate_resource_match(self, prm: ProtectedResourceMetadata) -> None:
486+
async def _validate_resource_match(self, prm: ProtectedResourceMetadata) -> None:
480487
"""Validate that PRM resource matches the server URL per RFC 8707."""
481-
if not prm.resource:
488+
prm_resource = str(prm.resource) if prm.resource else None
489+
490+
if self._validate_resource_url_callback is not None:
491+
await self._validate_resource_url_callback(self.context.server_url, prm_resource)
492+
return
493+
494+
if not prm_resource:
482495
return
483496
default_resource = resource_url_from_server_url(self.context.server_url)
484-
prm_resource = str(prm.resource)
485497
# Normalize: Pydantic AnyHttpUrl adds trailing slash to root URLs
486498
# (e.g. "https://example.com/") while resource_url_from_server_url may not.
487499
if not default_resource.endswith("/"):
@@ -533,7 +545,7 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
533545
prm = await handle_protected_resource_response(discovery_response)
534546
if prm:
535547
# Validate PRM resource matches server URL (RFC 8707)
536-
self._validate_resource_match(prm)
548+
await self._validate_resource_match(prm)
537549
self.context.protected_resource_metadata = prm
538550

539551
# todo: try all authorization_servers to find the OASM

0 commit comments

Comments
 (0)