Skip to content
4 changes: 2 additions & 2 deletions src/mcp/client/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,9 +421,9 @@ async def _handle_reconnection(
await event_source.response.aclose()
return

# Stream ended again without response - reconnect again (reset attempt counter)
# Stream ended again without response - reconnect again
logger.info("SSE stream disconnected, reconnecting...")
await self._handle_reconnection(ctx, reconnect_last_event_id, reconnect_retry_ms, 0)
await self._handle_reconnection(ctx, reconnect_last_event_id, reconnect_retry_ms, attempt + 1)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this about?

except Exception as e: # pragma: no cover
logger.debug(f"Reconnection failed: {e}")
# Try to reconnect again if we still have an event ID
Expand Down
14 changes: 9 additions & 5 deletions src/mcp/shared/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,15 @@ 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 None:
# Client registered without scope restrictions — allow any requested scope
return requested_scopes
requested = set(requested_scopes)
allowed = set(self.scope.split())
if not requested.issubset(allowed):
invalid = requested - allowed
raise InvalidScopeError(f"Client was not registered with scope(s): {' '.join(sorted(invalid))}")
return requested_scopes

def validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl:
if redirect_uri is not None:
Expand Down
38 changes: 37 additions & 1 deletion tests/shared/test_auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
"""Tests for OAuth 2.0 shared code."""

from mcp.shared.auth import OAuthMetadata
import pytest

from mcp.shared.auth import InvalidScopeError, OAuthClientInformationFull, OAuthMetadata


def _make_client(scope: str | None) -> OAuthClientInformationFull:
return OAuthClientInformationFull.model_validate(
{
"redirect_uris": ["https://example.com/callback"],
"scope": scope,
"client_id": "test-client",
}
)


def test_validate_scope_returns_none_when_no_scope_requested():
client = _make_client("read write")
assert client.validate_scope(None) is None


def test_validate_scope_allows_registered_scopes():
client = _make_client("read write")
assert client.validate_scope("read") == ["read"]
assert client.validate_scope("read write") == ["read", "write"]


def test_validate_scope_raises_for_unregistered_scope():
client = _make_client("read")
with pytest.raises(InvalidScopeError):
client.validate_scope("read admin")


def test_validate_scope_allows_any_scope_when_client_has_no_scope_restriction():
"""When client.scope is None, any requested scope should be allowed (issue #2216)."""
client = _make_client(None)
assert client.validate_scope("read") == ["read"]
assert client.validate_scope("read write admin") == ["read", "write", "admin"]


def test_oauth():
Expand Down
Loading