From 15f480a332fedd77b046839d9644cb5858bcb27d Mon Sep 17 00:00:00 2001 From: Maxwell Calkin <101308415+MaxwellCalkin@users.noreply.github.com> Date: Sun, 8 Mar 2026 01:51:33 -0500 Subject: [PATCH 1/2] fix: handle None required_scopes in validate_scope as no restrictions When a client is registered without scope restrictions (self.scope is None), validate_scope() incorrectly treated it as an empty allowed-scopes list, causing all requested scopes to be rejected with InvalidScopeError. Now when self.scope is None, the method returns the requested scopes as-is, treating None as 'no restrictions' rather than 'no scopes allowed'. Fixes #2216 --- src/mcp/shared/auth.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/mcp/shared/auth.py b/src/mcp/shared/auth.py index ca5b7b45a..429bebcd5 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 scope restrictions registered for this client; allow any scopes + 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: From 1f53180c0c50bd0e5ddd295019ecbca593609308 Mon Sep 17 00:00:00 2001 From: Maxwell Calkin <101308415+MaxwellCalkin@users.noreply.github.com> Date: Sun, 8 Mar 2026 01:52:37 -0500 Subject: [PATCH 2/2] test: add validate_scope unit tests for None scope handling Add regression tests for #2216 verifying that validate_scope() correctly allows any requested scopes when the client has no scope restrictions (self.scope is None). --- tests/shared/test_auth.py | 54 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/shared/test_auth.py b/tests/shared/test_auth.py index cd3c35332..b898b7280 100644 --- a/tests/shared/test_auth.py +++ b/tests/shared/test_auth.py @@ -1,6 +1,9 @@ """Tests for OAuth 2.0 shared code.""" -from mcp.shared.auth import OAuthMetadata +import pytest +from pydantic import AnyUrl + +from mcp.shared.auth import InvalidScopeError, OAuthClientMetadata, OAuthMetadata def test_oauth(): @@ -58,3 +61,52 @@ 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=None): + return OAuthClientMetadata( + redirect_uris=[AnyUrl("http://localhost:3000/callback")], + scope=scope, + ) + + def test_requested_none_returns_none(self): + """When no scope is requested, validate_scope returns None.""" + client = self._make_client(scope="read write") + assert client.validate_scope(None) is None + + def test_client_scope_none_allows_any_requested_scopes(self): + """When client has no scope restrictions (None), any requested scopes are allowed. + + Regression test for #2216: validate_scope treated None as empty list, + rejecting all scopes with InvalidScopeError. + """ + client = self._make_client(scope=None) + result = client.validate_scope("read write admin") + assert result == ["read", "write", "admin"] + + def test_client_scope_none_allows_single_scope(self): + """When client has no scope restrictions, a single requested scope is allowed.""" + client = self._make_client(scope=None) + result = client.validate_scope("read") + assert result == ["read"] + + def test_allowed_scopes_accepted(self): + """Requested scopes that are a subset of client scopes are accepted.""" + client = self._make_client(scope="read write admin") + result = client.validate_scope("read write") + assert result == ["read", "write"] + + def test_disallowed_scope_raises(self): + """Requesting a scope not in the client's registered scopes raises InvalidScopeError.""" + client = self._make_client(scope="read write") + with pytest.raises(InvalidScopeError, match="admin"): + client.validate_scope("read admin") + + def test_all_scopes_match(self): + """Requesting exactly the registered scopes works.""" + client = self._make_client(scope="read write") + result = client.validate_scope("read write") + assert result == ["read", "write"]