From d6bd5eb3d29885857aaee4a638cf857e80614599 Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 6 Mar 2026 09:25:17 +0530 Subject: [PATCH] fix: validate_scope treats None registered scopes as no restrictions When a client was registered without specific scopes (scope=None), validate_scope() converted None to an empty list, causing all requested scopes to be rejected with InvalidScopeError. Now None is correctly treated as "no restrictions", allowing any scope. Closes #2216 --- src/mcp/shared/auth.py | 9 ++++++--- tests/shared/test_auth.py | 40 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/mcp/shared/auth.py b/src/mcp/shared/auth.py index ca5b7b45a..fd68f9284 100644 --- a/src/mcp/shared/auth.py +++ b/src/mcp/shared/auth.py @@ -71,11 +71,14 @@ 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(" ") + if self.scope is None: + # No registered scopes means no restrictions + return requested_scopes + allowed_scopes = self.scope.split(" ") for scope in requested_scopes: - if scope not in allowed_scopes: # pragma: no branch + if scope not in allowed_scopes: raise InvalidScopeError(f"Client was not registered with scope {scope}") - return requested_scopes # pragma: no cover + 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..e02477deb 100644 --- a/tests/shared/test_auth.py +++ b/tests/shared/test_auth.py @@ -1,6 +1,8 @@ """Tests for OAuth 2.0 shared code.""" -from mcp.shared.auth import OAuthMetadata +import pytest + +from mcp.shared.auth import InvalidScopeError, OAuthClientMetadata, OAuthMetadata def test_oauth(): @@ -58,3 +60,39 @@ def test_oauth_with_jarm(): "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], } ) + + +class TestValidateScope: + """Tests for OAuthClientMetadata.validate_scope.""" + + def _make_client(self, scope: str | None = None) -> OAuthClientMetadata: + return OAuthClientMetadata.model_validate({"redirect_uris": ["https://example.com/callback"], "scope": scope}) + + def test_none_requested_scope_returns_none(self): + client = self._make_client(scope="read write") + assert client.validate_scope(None) is None + + def test_none_registered_scope_allows_any_requested_scope(self): + client = self._make_client(scope=None) + result = client.validate_scope("read write admin") + assert result == ["read", "write", "admin"] + + def test_registered_scope_allows_matching_requested_scope(self): + client = self._make_client(scope="read write") + result = client.validate_scope("read") + assert result == ["read"] + + def test_registered_scope_allows_all_matching_scopes(self): + client = self._make_client(scope="read write") + result = client.validate_scope("read write") + assert result == ["read", "write"] + + def test_registered_scope_rejects_unregistered_scope(self): + client = self._make_client(scope="read write") + with pytest.raises(InvalidScopeError, match="Client was not registered with scope admin"): + client.validate_scope("read admin") + + def test_empty_registered_scope_rejects_any_requested_scope(self): + client = self._make_client(scope="") + with pytest.raises(InvalidScopeError): + client.validate_scope("read")