FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(auth): preserve root protected resource URI by daleselaji-dev · Pull Request #3259 · modelcontextprotocol/python-sdk · GitHub

Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .py  (3) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
17 changes: 17 additions & 0 deletions src/mcp/shared/auth.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any, Literal, cast
from urllib.parse import urlsplit, urlunsplit

from pydantic import AnyHttpUrl, AnyUrl, BaseModel, ConfigDict, Field, field_validator, model_validator

Expand Down Expand Up @@ -256,3 +257,19 @@ class ProtectedResourceMetadata(BaseModel):
dpop_signing_alg_values_supported: list[str] | None = None
# dpop_bound_access_tokens_required default is False, but omitted here for clarity
dpop_bound_access_tokens_required: bool | None = None

@field_validator("resource", mode="before")
@classmethod
def _preserve_empty_resource_path(cls, value: object) -> object:
"""Keep the RFC 9728 root resource URI free of a synthetic slash.

``AnyHttpUrl`` normalizes ``https://example.com`` to
``https://example.com/`` before the model's ``url_preserve_empty_path``
setting can preserve the distinction. This is especially visible when
the value arrives as an already-validated ``AnyHttpUrl`` from the
server settings.
"""
parsed = urlsplit(str(value))
if parsed.path == "/":
return urlunsplit(parsed._replace(path=""))
return value
2 changes: 1 addition & 1 deletion tests/server/auth/test_protected_resource.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ async def test_metadata_endpoint_without_path(root_resource_client: httpx2.Async
assert response.status_code == 200
assert response.json() == snapshot(
{
"resource": "https://example.com/",
"resource": "https://example.com",
"authorization_servers": ["https://auth.example.com/"],
"scopes_supported": ["read"],
"resource_name": "Root Resource",
Expand Down
33 changes: 30 additions & 3 deletions tests/shared/test_auth.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
"""Tests for OAuth 2.0 shared code."""

import pytest
from pydantic import AnyUrl, ValidationError

from mcp.shared.auth import InvalidRedirectUriError, OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata
from pydantic import AnyHttpUrl, AnyUrl, ValidationError

from mcp.shared.auth import (
InvalidRedirectUriError,
OAuthClientInformationFull,
OAuthClientMetadata,
OAuthMetadata,
ProtectedResourceMetadata,
)


def test_oauth():
Expand Down Expand Up @@ -109,6 +115,27 @@ def test_valid_url_passes_through_unchanged():
assert str(metadata.client_uri) == "https://udemy.com/"


def test_protected_resource_metadata_preserves_empty_root_path():
metadata = ProtectedResourceMetadata.model_validate(
{
"resource": "https://example.com",
"authorization_servers": ["https://auth.example.com"],
}
)

assert str(metadata.resource) == "https://example.com"
assert '"resource":"https://example.com"' in metadata.model_dump_json()


def test_protected_resource_metadata_strips_normalized_root_path():
metadata = ProtectedResourceMetadata(
resource=AnyHttpUrl("https://example.com"),
authorization_servers=[AnyHttpUrl("https://auth.example.com")],
)

assert str(metadata.resource) == "https://example.com"


def test_information_full_inherits_coercion():
"""OAuthClientInformationFull shares the metadata base, so the same
coercion applies to DCR responses parsed via the full model."""
Expand Down
Loading

Back | FazBrowse Home | New Git URL