Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/mcp/client/auth/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
handle_token_response_scopes,
is_valid_client_metadata_url,
should_use_client_metadata_url,
union_scopes,
)
from mcp.client.streamable_http import MCP_PROTOCOL_VERSION
from mcp.shared.auth import (
Expand Down Expand Up @@ -624,20 +625,25 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
error = extract_field_from_www_auth(response, "error")

# Step 2: Check if we need to step-up authorization
if error == "insufficient_scope": # pragma: no branch
if error == "insufficient_scope":
try:
# Step 2a: Update the required scopes
self.context.client_metadata.scope = get_client_metadata_scopes(
# Step 2a: Union previously requested scopes with the
# step-up challenge so prior grants survive (SEP-2350).
challenge_scopes = get_client_metadata_scopes(
extract_scope_from_www_auth(response),
self.context.protected_resource_metadata,
self.context.oauth_metadata,
self.context.client_metadata.grant_types,
)
self.context.client_metadata.scope = union_scopes(
self.context.client_metadata.scope,
challenge_scopes,
)

# Step 2b: Perform (re-)authorization and token exchange
token_response = yield await self._perform_authorization()
await self._handle_token_response(token_response)
except Exception: # pragma: no cover
except Exception:
logger.exception("OAuth flow error")
raise

Expand Down
16 changes: 16 additions & 0 deletions src/mcp/client/auth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ def build_protected_resource_metadata_discovery_urls(www_auth_url: str | None, s
return urls


def union_scopes(existing: str | None, incoming: str | None) -> str | None:
"""Union two space-separated OAuth scope strings, preserving order."""
if existing is None:
return incoming
if incoming is None:
return existing

seen: set[str] = set()
merged: list[str] = []
for scope in existing.split() + incoming.split():
if scope not in seen:
seen.add(scope)
merged.append(scope)
return " ".join(merged)


def get_client_metadata_scopes(
www_authenticate_scope: str | None,
protected_resource_metadata: ProtectedResourceMetadata | None,
Expand Down
Loading
Loading