|
23 | 23 | from mcp.client.auth.utils import ( |
24 | 24 | build_oauth_authorization_server_metadata_discovery_urls, |
25 | 25 | build_protected_resource_metadata_discovery_urls, |
| 26 | + create_client_info_from_metadata_url, |
26 | 27 | create_client_registration_request, |
27 | 28 | create_oauth_metadata_request, |
28 | 29 | extract_field_from_www_auth, |
|
33 | 34 | handle_protected_resource_response, |
34 | 35 | handle_registration_response, |
35 | 36 | handle_token_response_scopes, |
| 37 | + is_valid_client_metadata_url, |
| 38 | + should_use_client_metadata_url, |
36 | 39 | ) |
37 | 40 | from mcp.client.streamable_http import MCP_PROTOCOL_VERSION |
38 | 41 | from mcp.shared.auth import ( |
@@ -96,6 +99,7 @@ class OAuthContext: |
96 | 99 | redirect_handler: Callable[[str], Awaitable[None]] | None |
97 | 100 | callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None |
98 | 101 | timeout: float = 300.0 |
| 102 | + client_metadata_url: str | None = None |
99 | 103 |
|
100 | 104 | # Discovered metadata |
101 | 105 | protected_resource_metadata: ProtectedResourceMetadata | None = None |
@@ -226,15 +230,40 @@ def __init__( |
226 | 230 | redirect_handler: Callable[[str], Awaitable[None]] | None = None, |
227 | 231 | callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None = None, |
228 | 232 | timeout: float = 300.0, |
| 233 | + client_metadata_url: str | None = None, |
229 | 234 | ): |
230 | | - """Initialize OAuth2 authentication.""" |
| 235 | + """Initialize OAuth2 authentication. |
| 236 | +
|
| 237 | + Args: |
| 238 | + server_url: The MCP server URL. |
| 239 | + client_metadata: OAuth client metadata for registration. |
| 240 | + storage: Token storage implementation. |
| 241 | + redirect_handler: Handler for authorization redirects. |
| 242 | + callback_handler: Handler for authorization callbacks. |
| 243 | + timeout: Timeout for the OAuth flow. |
| 244 | + client_metadata_url: URL-based client ID. When provided and the server |
| 245 | + advertises client_id_metadata_document_supported=true, this URL will be |
| 246 | + used as the client_id instead of performing dynamic client registration. |
| 247 | + Must be a valid HTTPS URL with a non-root pathname. |
| 248 | +
|
| 249 | + Raises: |
| 250 | + ValueError: If client_metadata_url is provided but not a valid HTTPS URL |
| 251 | + with a non-root pathname. |
| 252 | + """ |
| 253 | + # Validate client_metadata_url if provided |
| 254 | + if client_metadata_url is not None and not is_valid_client_metadata_url(client_metadata_url): |
| 255 | + raise ValueError( |
| 256 | + f"client_metadata_url must be a valid HTTPS URL with a non-root pathname, got: {client_metadata_url}" |
| 257 | + ) |
| 258 | + |
231 | 259 | self.context = OAuthContext( |
232 | 260 | server_url=server_url, |
233 | 261 | client_metadata=client_metadata, |
234 | 262 | storage=storage, |
235 | 263 | redirect_handler=redirect_handler, |
236 | 264 | callback_handler=callback_handler, |
237 | 265 | timeout=timeout, |
| 266 | + client_metadata_url=client_metadata_url, |
238 | 267 | ) |
239 | 268 | self._initialized = False |
240 | 269 |
|
@@ -566,17 +595,30 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx. |
566 | 595 | self.context.oauth_metadata, |
567 | 596 | ) |
568 | 597 |
|
569 | | - # Step 4: Register client if needed |
570 | | - registration_request = create_client_registration_request( |
571 | | - self.context.oauth_metadata, |
572 | | - self.context.client_metadata, |
573 | | - self.context.get_authorization_base_url(self.context.server_url), |
574 | | - ) |
| 598 | + # Step 4: Register client or use URL-based client ID (CIMD) |
575 | 599 | if not self.context.client_info: |
576 | | - registration_response = yield registration_request |
577 | | - client_information = await handle_registration_response(registration_response) |
578 | | - self.context.client_info = client_information |
579 | | - await self.context.storage.set_client_info(client_information) |
| 600 | + if should_use_client_metadata_url( |
| 601 | + self.context.oauth_metadata, self.context.client_metadata_url |
| 602 | + ): |
| 603 | + # Use URL-based client ID (CIMD) |
| 604 | + logger.debug(f"Using URL-based client ID (CIMD): {self.context.client_metadata_url}") |
| 605 | + client_information = create_client_info_from_metadata_url( |
| 606 | + self.context.client_metadata_url, # type: ignore[arg-type] |
| 607 | + redirect_uris=self.context.client_metadata.redirect_uris, |
| 608 | + ) |
| 609 | + self.context.client_info = client_information |
| 610 | + await self.context.storage.set_client_info(client_information) |
| 611 | + else: |
| 612 | + # Fallback to Dynamic Client Registration |
| 613 | + registration_request = create_client_registration_request( |
| 614 | + self.context.oauth_metadata, |
| 615 | + self.context.client_metadata, |
| 616 | + self.context.get_authorization_base_url(self.context.server_url), |
| 617 | + ) |
| 618 | + registration_response = yield registration_request |
| 619 | + client_information = await handle_registration_response(registration_response) |
| 620 | + self.context.client_info = client_information |
| 621 | + await self.context.storage.set_client_info(client_information) |
580 | 622 |
|
581 | 623 | # Step 5: Perform authorization and complete token exchange |
582 | 624 | token_response = yield await self._perform_authorization() |
|
0 commit comments