Skip to content

Commit 5cfe6aa

Browse files
Fix validate_scope rejecting scopes when client scope is None
When OAuthClientMetadata.scope is None (no scopes registered), validate_scope() was converting it to an empty list, causing all requested scopes to be rejected with InvalidScopeError. Now treats None as "no restrictions" and allows any requested scope through. Fixes #2216
1 parent 3d7b311 commit 5cfe6aa

File tree

2 files changed

+55
-3
lines changed

2 files changed

+55
-3
lines changed

src/mcp/shared/auth.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,14 @@ def validate_scope(self, requested_scope: str | None) -> list[str] | None:
8989
if requested_scope is None:
9090
return None
9191
requested_scopes = requested_scope.split(" ")
92-
allowed_scopes = [] if self.scope is None else self.scope.split(" ")
92+
if self.scope is None:
93+
# No registered scopes means no restrictions
94+
return requested_scopes
95+
allowed_scopes = self.scope.split(" ")
9396
for scope in requested_scopes:
94-
if scope not in allowed_scopes: # pragma: no branch
97+
if scope not in allowed_scopes:
9598
raise InvalidScopeError(f"Client was not registered with scope {scope}")
96-
return requested_scopes # pragma: no cover
99+
return requested_scopes
97100

98101
def validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl:
99102
if redirect_uri is not None:

tests/shared/test_auth.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,52 @@ def test_invalid_non_empty_url_still_rejected():
138138
}
139139
with pytest.raises(ValidationError):
140140
OAuthClientMetadata.model_validate(data)
141+
142+
143+
class TestValidateScope:
144+
"""Tests for OAuthClientMetadata.validate_scope()."""
145+
146+
def _make_client(self, scope: str | None = None) -> OAuthClientMetadata:
147+
return OAuthClientMetadata.model_validate(
148+
{
149+
"redirect_uris": ["https://example.com/callback"],
150+
"scope": scope,
151+
}
152+
)
153+
154+
def test_requested_scope_none_returns_none(self):
155+
client = self._make_client(scope="read write")
156+
assert client.validate_scope(None) is None
157+
158+
def test_registered_scope_none_allows_any_requested_scope(self):
159+
"""When the client has no registered scopes (scope=None),
160+
any requested scope should be allowed through."""
161+
client = self._make_client(scope=None)
162+
result = client.validate_scope("read write admin")
163+
assert result == ["read", "write", "admin"]
164+
165+
def test_registered_scope_none_allows_single_scope(self):
166+
client = self._make_client(scope=None)
167+
result = client.validate_scope("read")
168+
assert result == ["read"]
169+
170+
def test_valid_scope_subset(self):
171+
client = self._make_client(scope="read write admin")
172+
result = client.validate_scope("read write")
173+
assert result == ["read", "write"]
174+
175+
def test_valid_scope_exact_match(self):
176+
client = self._make_client(scope="read write")
177+
result = client.validate_scope("read write")
178+
assert result == ["read", "write"]
179+
180+
def test_invalid_scope_raises_error(self):
181+
from mcp.shared.auth import InvalidScopeError
182+
183+
client = self._make_client(scope="read write")
184+
with pytest.raises(InvalidScopeError, match="delete"):
185+
client.validate_scope("read delete")
186+
187+
def test_no_registered_scope_and_no_requested_scope(self):
188+
client = self._make_client(scope=None)
189+
assert client.validate_scope(None) is None

0 commit comments

Comments
 (0)