diff --git a/src/mcp/server/auth/routes.py b/src/mcp/server/auth/routes.py index 4c56ca247..b12199f21 100644 --- a/src/mcp/server/auth/routes.py +++ b/src/mcp/server/auth/routes.py @@ -147,31 +147,19 @@ def create_auth_routes( return routes -def modify_url_path(url: AnyHttpUrl, path_mapper: Callable[[str], str]) -> AnyHttpUrl: - return AnyHttpUrl.build( - scheme=url.scheme, - username=url.username, - password=url.password, - host=url.host, - port=url.port, - path=path_mapper(url.path or ""), - query=url.query, - fragment=url.fragment, - ) - - def build_metadata( issuer_url: AnyHttpUrl, service_documentation_url: AnyHttpUrl | None, client_registration_options: ClientRegistrationOptions, revocation_options: RevocationOptions, ) -> OAuthMetadata: - authorization_url = modify_url_path( - issuer_url, lambda path: path.rstrip("/") + AUTHORIZATION_PATH.lstrip("/") + authorization_url = AnyHttpUrl( + str(issuer_url).rstrip("/") + AUTHORIZATION_PATH ) - token_url = modify_url_path( - issuer_url, lambda path: path.rstrip("/") + TOKEN_PATH.lstrip("/") + token_url = AnyHttpUrl( + str(issuer_url).rstrip("/") + TOKEN_PATH ) + # Create metadata metadata = OAuthMetadata( issuer=issuer_url, @@ -193,14 +181,14 @@ def build_metadata( # Add registration endpoint if supported if client_registration_options.enabled: - metadata.registration_endpoint = modify_url_path( - issuer_url, lambda path: path.rstrip("/") + REGISTRATION_PATH.lstrip("/") + metadata.registration_endpoint = AnyHttpUrl( + str(issuer_url).rstrip("/") + REGISTRATION_PATH ) # Add revocation endpoint if supported if revocation_options.enabled: - metadata.revocation_endpoint = modify_url_path( - issuer_url, lambda path: path.rstrip("/") + REVOCATION_PATH.lstrip("/") + metadata.revocation_endpoint = AnyHttpUrl( + str(issuer_url).rstrip("/") + REVOCATION_PATH ) metadata.revocation_endpoint_auth_methods_supported = ["client_secret_post"] diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 996534e9c..781ec2129 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -10,9 +10,12 @@ import httpx import pytest +from inline_snapshot import snapshot from pydantic import AnyHttpUrl from mcp.client.auth import OAuthClientProvider +from mcp.server.auth.routes import build_metadata +from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions from mcp.shared.auth import ( OAuthClientInformationFull, OAuthClientMetadata, @@ -905,3 +908,76 @@ async def test_token_exchange_error_basic(self, oauth_provider, oauth_client_inf await oauth_provider._exchange_code_for_token( "invalid_auth_code", oauth_client_info ) + + +@pytest.mark.parametrize( + ( + "issuer_url", + "service_documentation_url", + "authorization_endpoint", + "token_endpoint", + "registration_endpoint", + "revocation_endpoint", + ), + ( + pytest.param( + "https://auth.example.com", + "https://auth.example.com/docs", + "https://auth.example.com/authorize", + "https://auth.example.com/token", + "https://auth.example.com/register", + "https://auth.example.com/revoke", + id="simple-url", + ), + pytest.param( + "https://auth.example.com/", + "https://auth.example.com/docs", + "https://auth.example.com/authorize", + "https://auth.example.com/token", + "https://auth.example.com/register", + "https://auth.example.com/revoke", + id="with-trailing-slash", + ), + pytest.param( + "https://auth.example.com/v1/mcp", + "https://auth.example.com/v1/mcp/docs", + "https://auth.example.com/v1/mcp/authorize", + "https://auth.example.com/v1/mcp/token", + "https://auth.example.com/v1/mcp/register", + "https://auth.example.com/v1/mcp/revoke", + id="with-path-param", + ), + ), +) +def test_build_metadata( + issuer_url: str, + service_documentation_url: str, + authorization_endpoint: str, + token_endpoint: str, + registration_endpoint: str, + revocation_endpoint: str, +): + metadata = build_metadata( + issuer_url=AnyHttpUrl(issuer_url), + service_documentation_url=AnyHttpUrl(service_documentation_url), + client_registration_options=ClientRegistrationOptions( + enabled=True, valid_scopes=["read", "write", "admin"] + ), + revocation_options=RevocationOptions(enabled=True), + ) + + assert metadata == snapshot( + OAuthMetadata( + issuer=AnyHttpUrl(issuer_url), + authorization_endpoint=AnyHttpUrl(authorization_endpoint), + token_endpoint=AnyHttpUrl(token_endpoint), + registration_endpoint=AnyHttpUrl(registration_endpoint), + scopes_supported=["read", "write", "admin"], + grant_types_supported=["authorization_code", "refresh_token"], + token_endpoint_auth_methods_supported=["client_secret_post"], + service_documentation=AnyHttpUrl(service_documentation_url), + revocation_endpoint=AnyHttpUrl(revocation_endpoint), + revocation_endpoint_auth_methods_supported=["client_secret_post"], + code_challenge_methods_supported=["S256"], + ) + )