diff --git a/docs/python-sdk/fastmcp-exceptions.mdx b/docs/python-sdk/fastmcp-exceptions.mdx
index c7dcfeb93..a6ef59df9 100644
--- a/docs/python-sdk/fastmcp-exceptions.mdx
+++ b/docs/python-sdk/fastmcp-exceptions.mdx
@@ -10,7 +10,7 @@ Custom exceptions for FastMCP.
## Functions
-### `to_mcp_error`
+### `to_mcp_error`
```python
to_mcp_error(exc: Exception) -> MCPError
@@ -119,3 +119,16 @@ or policy tripped.
Error when authorization check fails.
+
+### `InsufficientScopeError`
+
+
+Authorization failed because the token is missing required OAuth scopes.
+
+Unlike a bare ``AuthorizationError``, this carries the specific scopes the
+caller must obtain. A component-level scope shortfall can then be signalled
+as a spec-correct ``insufficient_scope`` step-up (SEP-2350 / RFC 6750 §3),
+naming exactly what to re-authorize for instead of an opaque denial. The
+named scopes are only the *unmet* ones, so an existing grant is accumulated
+rather than replaced when the caller re-authorizes.
+
diff --git a/docs/python-sdk/fastmcp-utilities-authorization.mdx b/docs/python-sdk/fastmcp-utilities-authorization.mdx
index f70c44283..0f64a0a4a 100644
--- a/docs/python-sdk/fastmcp-utilities-authorization.mdx
+++ b/docs/python-sdk/fastmcp-utilities-authorization.mdx
@@ -15,7 +15,7 @@ deny with a custom message; other exceptions are masked and treated as denial.
## Functions
-### `require_scopes`
+### `require_scopes`
```python
require_scopes(*scopes: str) -> AuthCheck
@@ -25,7 +25,52 @@ require_scopes(*scopes: str) -> AuthCheck
Require all of the given OAuth scopes.
-### `restrict_tag`
+### `require_roles`
+
+```python
+require_roles(*roles: str) -> AuthCheck
+```
+
+
+Require all of the given roles, read from the token's claims.
+
+Roles and groups are not part of OIDC, so every identity provider puts them
+somewhere different: `realm_access.roles` on Keycloak, `roles` on Microsoft
+Entra, `cognito:groups` on AWS Cognito, `permissions` or a namespaced custom
+claim on Auth0. `extract` receives the token's claims and returns the
+caller's roles, which keeps that provider-specific knowledge at the call
+site instead of guessing it here.
+
+```python
+from fastmcp.server.auth import require_roles
+
+keycloak = require_roles("admin", extract=lambda c: c["realm_access"]["roles"])
+cognito = require_roles("admins", extract=lambda c: c["cognito:groups"])
+```
+
+A token missing the claim entirely is denied rather than treated as an
+error, so `extract` may index into the claims without guarding. An
+extractor returning a bare string is treated as one role, since a provider
+that stores a single role as a scalar is common.
+
+Unlike `require_scopes`, this check cannot signal a shortfall: OAuth has no
+way to request a role, so there is no `insufficient_scope` challenge to
+emit. A role denial is therefore reported as a plain `AuthorizationError`,
+and it suppresses any scope shortfall alongside it — a caller blocked by
+their role must not be told to go obtain a scope that would not help.
+Scope shortfalls are still reported normally whenever the role check
+passes.
+
+**Args:**
+- `*roles`: Roles the caller must hold. All are required (AND logic).
+- `extract`: Callable mapping the token's claims to the caller's roles.
+
+**Raises:**
+- `ValueError`: If no roles are given, which would allow any authenticated
+caller and is more likely a mistake than an intent.
+
+
+### `restrict_tag`
```python
restrict_tag(tag: str) -> AuthCheck
@@ -35,14 +80,62 @@ restrict_tag(tag: str) -> AuthCheck
Require scopes when the accessed component has a specific tag.
-### `run_auth_checks`
+### `scope_requirements`
+
+```python
+scope_requirements(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> list[str] | None
+```
+
+
+Scopes a check list requires but the token lacks, without running it.
+
+Returns ``None`` when the list contains any opaque (non-scope) check. Such a
+check might deny for a reason unrelated to scopes, and evaluating it here
+would run authorization logic — with whatever side effects it carries —
+outside its normal place in the chain. Since its verdict is unknown, its
+siblings' scopes must not be disclosed either, so the whole list is withheld.
+
+When every check is scope-aware, the result is their combined shortfall,
+computed purely from the token and component (an empty list means the list is
+already satisfied). This lets a shortfall be aggregated across authorization
+layers without evaluating anything that would otherwise be skipped.
+
+
+### `run_auth_checks_with_shortfall`
+
+```python
+run_auth_checks_with_shortfall(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> tuple[bool, list[str]]
+```
+
+
+Run auth checks with AND logic, classifying the denial cause.
+
+Returns ``(authorized, missing_scopes)``. ``missing_scopes`` names every
+scope the caller must obtain to satisfy *all* scope requirements at once:
+the union of the shortfalls across every scope-aware check, not just the
+first one to fail. Reporting only the first would strand a caller in a
+step-up loop — it obtains that scope, retries, and is denied again for the
+next — so the union is what makes a single re-authorization converge.
+
+The challenge is withheld entirely (an empty list, which the caller surfaces
+as a plain ``AuthorizationError``) unless every non-scope check passes. A
+custom policy denial — a tenant check, say — must never be reported as an
+``insufficient_scope`` shortfall, and must never name the scopes of a
+component the caller could not otherwise reach. To guarantee that, the
+opaque checks are all evaluated before any scope is disclosed; a shortfall
+is only reported once they have all passed.
+
+An ``AuthorizationError`` raised by a check propagates unchanged.
+
+
+### `run_auth_checks`
```python
run_auth_checks(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> bool
```
-Run auth checks with AND logic.
+Run auth checks with AND logic, stopping at the first failure.
## Classes
diff --git a/docs/python-sdk/fastmcp-utilities-json_schema.mdx b/docs/python-sdk/fastmcp-utilities-json_schema.mdx
index f4f952a2a..8f8e580bf 100644
--- a/docs/python-sdk/fastmcp-utilities-json_schema.mdx
+++ b/docs/python-sdk/fastmcp-utilities-json_schema.mdx
@@ -7,7 +7,7 @@ sidebarTitle: json_schema
## Functions
-### `require_discriminator_property`
+### `require_discriminator_property`
```python
require_discriminator_property(schema: dict[str, Any]) -> dict[str, Any]
@@ -24,7 +24,7 @@ model with ``union_tag_not_found``. No-op if there is no string
``propertyName``.
-### `dereference_refs`
+### `dereference_refs`
```python
dereference_refs(schema: dict[str, Any]) -> dict[str, Any]
@@ -57,7 +57,7 @@ schemas from untrusted servers.
- when no longer needed
-### `resolve_root_ref`
+### `resolve_root_ref`
```python
resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]
@@ -79,7 +79,7 @@ the referenced definition while preserving $defs for nested references.
- if no resolution is needed
-### `compress_schema`
+### `compress_schema`
```python
compress_schema(schema: dict[str, Any], prune_params: list[str] | None = None, prune_additional_properties: bool = False, prune_titles: bool = False, dereference: bool = False) -> dict[str, Any]
diff --git a/docs/python-sdk/fastmcp-utilities-types.mdx b/docs/python-sdk/fastmcp-utilities-types.mdx
index 34d543b4b..6b8ca2ce8 100644
--- a/docs/python-sdk/fastmcp-utilities-types.mdx
+++ b/docs/python-sdk/fastmcp-utilities-types.mdx
@@ -77,7 +77,7 @@ This is used to exclude parameters from type adapter processing when they can't
The excluded parameters are removed from the function's __annotations__ dictionary.
-### `replace_type`
+### `replace_type`
```python
replace_type(type_, type_map: dict[type, type])
@@ -145,13 +145,13 @@ Helper class for returning audio from tools.
**Methods:**
-#### `to_audio_content`
+#### `to_audio_content`
```python
to_audio_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp_types.AudioContent
```
-### `File`
+### `File`
Helper class for returning file data from tools.
@@ -159,10 +159,10 @@ Helper class for returning file data from tools.
**Methods:**
-#### `to_resource_content`
+#### `to_resource_content`
```python
to_resource_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp_types.EmbeddedResource
```
-### `ContextSamplingFallbackProtocol`
+### `ContextSamplingFallbackProtocol`