Skip to content

Commit 4cbd8cb

Browse files
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
1 parent d5b9155 commit 4cbd8cb

File tree

2 files changed

+56
-5
lines changed

2 files changed

+56
-5
lines changed

src/mcp/shared/auth.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,12 @@ def validate_scope(self, requested_scope: str | None) -> list[str] | None:
7171
if requested_scope is None:
7272
return None
7373
requested_scopes = requested_scope.split(" ")
74-
allowed_scopes = [] if self.scope is None else self.scope.split(" ")
75-
for scope in requested_scopes:
76-
if scope not in allowed_scopes: # pragma: no branch
77-
raise InvalidScopeError(f"Client was not registered with scope {scope}")
78-
return requested_scopes # pragma: no cover
74+
if self.scope is not None:
75+
allowed_scopes = self.scope.split(" ")
76+
for scope in requested_scopes:
77+
if scope not in allowed_scopes:
78+
raise InvalidScopeError(f"Client was not registered with scope {scope}")
79+
return requested_scopes
7980

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

tests/shared/test_auth.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,53 @@ def test_oauth_with_jarm():
5858
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
5959
}
6060
)
61+
62+
63+
def test_validate_scope_none_client_scope_allows_any_requested_scope():
64+
"""When client.scope is None, any requested scope should be allowed."""
65+
from mcp.shared.auth import OAuthClientMetadata
66+
67+
client = OAuthClientMetadata(
68+
redirect_uris=["https://example.com/callback"],
69+
scope=None,
70+
)
71+
result = client.validate_scope("read write admin")
72+
assert result == ["read", "write", "admin"]
73+
74+
75+
def test_validate_scope_with_client_scope_rejects_unregistered():
76+
"""When client.scope is set, unregistered scopes should be rejected."""
77+
import pytest
78+
79+
from mcp.shared.auth import InvalidScopeError, OAuthClientMetadata
80+
81+
client = OAuthClientMetadata(
82+
redirect_uris=["https://example.com/callback"],
83+
scope="read write",
84+
)
85+
with pytest.raises(InvalidScopeError):
86+
client.validate_scope("admin")
87+
88+
89+
def test_validate_scope_with_client_scope_allows_registered():
90+
"""When client.scope is set, registered scopes should be allowed."""
91+
from mcp.shared.auth import OAuthClientMetadata
92+
93+
client = OAuthClientMetadata(
94+
redirect_uris=["https://example.com/callback"],
95+
scope="read write",
96+
)
97+
result = client.validate_scope("read")
98+
assert result == ["read"]
99+
100+
101+
def test_validate_scope_none_requested_returns_none():
102+
"""When requested_scope is None, should return None."""
103+
from mcp.shared.auth import OAuthClientMetadata
104+
105+
client = OAuthClientMetadata(
106+
redirect_uris=["https://example.com/callback"],
107+
scope=None,
108+
)
109+
result = client.validate_scope(None)
110+
assert result is None

0 commit comments

Comments
 (0)