From 4cbd8cbaadc4332c8d16b63e88f679094abc4f68 Mon Sep 17 00:00:00 2001 From: Christian Sidak Date: Sun, 12 Apr 2026 11:23:27 -0700 Subject: [PATCH] Fix validate_scope rejecting scopes when client scope is None When a client is registered without specifying allowed scopes (scope=None), validate_scope was treating this as an empty allowlist, rejecting all requested scopes with InvalidScopeError. The correct behavior per OAuth 2.0 semantics is to treat None as unrestricted, allowing any requested scope. Fixes #2216 --- src/mcp/shared/auth.py | 11 +++++---- tests/shared/test_auth.py | 50 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/mcp/shared/auth.py b/src/mcp/shared/auth.py index ca5b7b45a..5749dc45e 100644 --- a/src/mcp/shared/auth.py +++ b/src/mcp/shared/auth.py @@ -71,11 +71,12 @@ def validate_scope(self, requested_scope: str | None) -> list[str] | None: if requested_scope is None: return None requested_scopes = requested_scope.split(" ") - allowed_scopes = [] if self.scope is None else self.scope.split(" ") - for scope in requested_scopes: - if scope not in allowed_scopes: # pragma: no branch - raise InvalidScopeError(f"Client was not registered with scope {scope}") - return requested_scopes # pragma: no cover + if self.scope is not None: + allowed_scopes = self.scope.split(" ") + for scope in requested_scopes: + if scope not in allowed_scopes: + raise InvalidScopeError(f"Client was not registered with scope {scope}") + return requested_scopes def validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl: if redirect_uri is not None: diff --git a/tests/shared/test_auth.py b/tests/shared/test_auth.py index cd3c35332..7b85a88da 100644 --- a/tests/shared/test_auth.py +++ b/tests/shared/test_auth.py @@ -58,3 +58,53 @@ def test_oauth_with_jarm(): "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], } ) + + +def test_validate_scope_none_client_scope_allows_any_requested_scope(): + """When client.scope is None, any requested scope should be allowed.""" + from mcp.shared.auth import OAuthClientMetadata + + client = OAuthClientMetadata( + redirect_uris=["https://example.com/callback"], + scope=None, + ) + result = client.validate_scope("read write admin") + assert result == ["read", "write", "admin"] + + +def test_validate_scope_with_client_scope_rejects_unregistered(): + """When client.scope is set, unregistered scopes should be rejected.""" + import pytest + + from mcp.shared.auth import InvalidScopeError, OAuthClientMetadata + + client = OAuthClientMetadata( + redirect_uris=["https://example.com/callback"], + scope="read write", + ) + with pytest.raises(InvalidScopeError): + client.validate_scope("admin") + + +def test_validate_scope_with_client_scope_allows_registered(): + """When client.scope is set, registered scopes should be allowed.""" + from mcp.shared.auth import OAuthClientMetadata + + client = OAuthClientMetadata( + redirect_uris=["https://example.com/callback"], + scope="read write", + ) + result = client.validate_scope("read") + assert result == ["read"] + + +def test_validate_scope_none_requested_returns_none(): + """When requested_scope is None, should return None.""" + from mcp.shared.auth import OAuthClientMetadata + + client = OAuthClientMetadata( + redirect_uris=["https://example.com/callback"], + scope=None, + ) + result = client.validate_scope(None) + assert result is None