Skip to content
Closed
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
6 changes: 6 additions & 0 deletions src/mcp/server/auth/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ class AuthSettings(BaseModel):
client_registration_options: ClientRegistrationOptions | None = None
revocation_options: RevocationOptions | None = None
required_scopes: list[str] | None = None
scopes_supported: list[str] | None = Field(
default=None,
description="Scopes advertised in the Protected Resource Metadata. Defaults to "
"`required_scopes`; set it to also advertise scopes that individual handlers check "
"without requiring them on every request.",
)
identity_assertion_enabled: bool = Field(
default=False,
description="Advertise and accept the SEP-990 Identity Assertion Authorization Grant "
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/server/lowlevel/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ def streamable_http_app(
create_protected_resource_routes(
resource_url=auth.resource_server_url,
authorization_servers=[auth.issuer_url],
scopes_supported=auth.required_scopes,
scopes_supported=auth.scopes_supported or auth.required_scopes,
)
)

Expand Down
2 changes: 1 addition & 1 deletion src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1275,7 +1275,7 @@ async def sse_endpoint(request: Request) -> Response: # pragma: no cover
create_protected_resource_routes(
resource_url=self.settings.auth.resource_server_url,
authorization_servers=[self.settings.auth.issuer_url],
scopes_supported=self.settings.auth.required_scopes,
scopes_supported=self.settings.auth.scopes_supported or self.settings.auth.required_scopes,
)
)

Expand Down
59 changes: 59 additions & 0 deletions tests/server/auth/test_protected_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,65 @@
from mcp.server.auth.routes import build_resource_metadata_url, create_protected_resource_routes


class _RejectingTokenVerifier:
async def verify_token(self, token: str):
return None


@pytest.mark.anyio
async def test_scopes_supported_advertised_separately_from_required_scopes():
"""A server can advertise scopes it does not challenge for on every request (#3778)."""
from mcp.server.auth.settings import AuthSettings
from mcp.server.mcpserver import MCPServer

mcp = MCPServer(
"test",
auth=AuthSettings(
issuer_url=AnyHttpUrl("https://auth.example.com"),
resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
validate_token_resource=True,
required_scopes=["returns:read"],
scopes_supported=["returns:read", "returns:write"],
),
token_verifier=_RejectingTokenVerifier(),
)

async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=mcp.streamable_http_app()),
base_url="http://127.0.0.1:8000",
) as client:
response = await client.get("/.well-known/oauth-protected-resource/mcp")

assert response.status_code == 200
assert response.json()["scopes_supported"] == ["returns:read", "returns:write"]


@pytest.mark.anyio
async def test_scopes_supported_defaults_to_required_scopes():
from mcp.server.auth.settings import AuthSettings
from mcp.server.mcpserver import MCPServer

mcp = MCPServer(
"test",
auth=AuthSettings(
issuer_url=AnyHttpUrl("https://auth.example.com"),
resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
validate_token_resource=True,
required_scopes=["returns:read"],
),
token_verifier=_RejectingTokenVerifier(),
)

async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=mcp.streamable_http_app()),
base_url="http://127.0.0.1:8000",
) as client:
response = await client.get("/.well-known/oauth-protected-resource/mcp")

assert response.status_code == 200
assert response.json()["scopes_supported"] == ["returns:read"]


@pytest.fixture
def test_app():
"""Fixture to create protected resource routes for testing."""
Expand Down
Loading