|
6 | 6 |
|
7 | 7 | import httpx
|
8 | 8 | import pytest
|
| 9 | +from inline_snapshot import Is, snapshot |
9 | 10 | from pydantic import AnyHttpUrl, AnyUrl
|
10 | 11 |
|
11 | 12 | from mcp.client.auth import OAuthClientProvider, PKCEParameters
|
@@ -580,3 +581,82 @@ async def test_auth_flow_with_valid_tokens(self, oauth_provider, mock_storage, v
|
580 | 581 | await auth_flow.asend(response)
|
581 | 582 | except StopAsyncIteration:
|
582 | 583 | pass # Expected
|
| 584 | + |
| 585 | + |
| 586 | +@pytest.mark.parametrize( |
| 587 | + ( |
| 588 | + "issuer_url", |
| 589 | + "service_documentation_url", |
| 590 | + "authorization_endpoint", |
| 591 | + "token_endpoint", |
| 592 | + "registration_endpoint", |
| 593 | + "revocation_endpoint", |
| 594 | + ), |
| 595 | + ( |
| 596 | + # Pydantic's AnyUrl incorrectly adds trailing slash to base URLs |
| 597 | + # This is being fixed in https://github.com/pydantic/pydantic-core/pull/1719 (Pydantic 2.12+) |
| 598 | + pytest.param( |
| 599 | + "https://auth.example.com", |
| 600 | + "https://auth.example.com/docs", |
| 601 | + "https://auth.example.com/authorize", |
| 602 | + "https://auth.example.com/token", |
| 603 | + "https://auth.example.com/register", |
| 604 | + "https://auth.example.com/revoke", |
| 605 | + id="simple-url", |
| 606 | + marks=pytest.mark.xfail( |
| 607 | + reason="Pydantic AnyUrl adds trailing slash to base URLs - fixed in Pydantic 2.12+" |
| 608 | + ), |
| 609 | + ), |
| 610 | + pytest.param( |
| 611 | + "https://auth.example.com/", |
| 612 | + "https://auth.example.com/docs", |
| 613 | + "https://auth.example.com/authorize", |
| 614 | + "https://auth.example.com/token", |
| 615 | + "https://auth.example.com/register", |
| 616 | + "https://auth.example.com/revoke", |
| 617 | + id="with-trailing-slash", |
| 618 | + ), |
| 619 | + pytest.param( |
| 620 | + "https://auth.example.com/v1/mcp", |
| 621 | + "https://auth.example.com/v1/mcp/docs", |
| 622 | + "https://auth.example.com/v1/mcp/authorize", |
| 623 | + "https://auth.example.com/v1/mcp/token", |
| 624 | + "https://auth.example.com/v1/mcp/register", |
| 625 | + "https://auth.example.com/v1/mcp/revoke", |
| 626 | + id="with-path-param", |
| 627 | + ), |
| 628 | + ), |
| 629 | +) |
| 630 | +def test_build_metadata( |
| 631 | + issuer_url: str, |
| 632 | + service_documentation_url: str, |
| 633 | + authorization_endpoint: str, |
| 634 | + token_endpoint: str, |
| 635 | + registration_endpoint: str, |
| 636 | + revocation_endpoint: str, |
| 637 | +): |
| 638 | + from mcp.server.auth.routes import build_metadata |
| 639 | + from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions |
| 640 | + |
| 641 | + metadata = build_metadata( |
| 642 | + issuer_url=AnyHttpUrl(issuer_url), |
| 643 | + service_documentation_url=AnyHttpUrl(service_documentation_url), |
| 644 | + client_registration_options=ClientRegistrationOptions(enabled=True, valid_scopes=["read", "write", "admin"]), |
| 645 | + revocation_options=RevocationOptions(enabled=True), |
| 646 | + ) |
| 647 | + |
| 648 | + assert metadata.model_dump(exclude_defaults=True, mode="json") == snapshot( |
| 649 | + { |
| 650 | + "issuer": Is(issuer_url), |
| 651 | + "authorization_endpoint": Is(authorization_endpoint), |
| 652 | + "token_endpoint": Is(token_endpoint), |
| 653 | + "registration_endpoint": Is(registration_endpoint), |
| 654 | + "scopes_supported": ["read", "write", "admin"], |
| 655 | + "grant_types_supported": ["authorization_code", "refresh_token"], |
| 656 | + "token_endpoint_auth_methods_supported": ["client_secret_post"], |
| 657 | + "service_documentation": Is(service_documentation_url), |
| 658 | + "revocation_endpoint": Is(revocation_endpoint), |
| 659 | + "revocation_endpoint_auth_methods_supported": ["client_secret_post"], |
| 660 | + "code_challenge_methods_supported": ["S256"], |
| 661 | + } |
| 662 | + ) |
0 commit comments