Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions 6 src/mcp/client/auth/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,12 +493,6 @@ async def _validate_resource_match(self, prm: ProtectedResourceMetadata) -> None
if not prm_resource:
return # pragma: no cover
default_resource = resource_url_from_server_url(self.context.server_url)
# Normalize: Pydantic AnyHttpUrl adds trailing slash to root URLs
# (e.g. "https://example.com/") while resource_url_from_server_url may not.
if not default_resource.endswith("/"):
default_resource += "/"
if not prm_resource.endswith("/"):
prm_resource += "/"
if not check_resource_allowed(requested_resource=default_resource, configured_resource=prm_resource):
raise OAuthFlowError(f"Protected resource {prm_resource} does not match expected {default_resource}")

Expand Down
13 changes: 4 additions & 9 deletions 13 src/mcp/shared/auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,17 @@ def check_resource_allowed(requested_resource: str, configured_resource: str) ->
if requested.scheme.lower() != configured.scheme.lower() or requested.netloc.lower() != configured.netloc.lower():
return False

# Handle cases like requested=/foo and configured=/foo/
# Normalize trailing slashes before comparison so that
# "/foo" and "/foo/" are treated as equivalent.
requested_path = requested.path
configured_path = configured.path

# If requested path is shorter, it cannot be a child
if len(requested_path) < len(configured_path):
return False

# Check if the requested path starts with the configured path
# Ensure both paths end with / for proper comparison
# This ensures that paths like "/api123" don't incorrectly match "/api"
if not requested_path.endswith("/"):
requested_path += "/"
if not configured_path.endswith("/"):
configured_path += "/"

# Check hierarchical match: requested must start with configured path.
# The trailing-slash normalization ensures "/api123/" won't match "/api/".
return requested_path.startswith(configured_path)


Expand Down
2 changes: 1 addition & 1 deletion 2 tests/shared/test_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def test_check_resource_allowed_trailing_slash_handling():
"""Trailing slashes should be handled correctly."""
# With and without trailing slashes
assert check_resource_allowed("https://example.com/api/", "https://example.com/api") is True
assert check_resource_allowed("https://example.com/api", "https://example.com/api/") is False
assert check_resource_allowed("https://example.com/api", "https://example.com/api/") is True
assert check_resource_allowed("https://example.com/api/v1", "https://example.com/api") is True
assert check_resource_allowed("https://example.com/api/v1", "https://example.com/api/") is True

Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.