diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx
index 05399fce69..432b191bd8 100644
--- a/docs/development/v3-notes/v3-features.mdx
+++ b/docs/development/v3-notes/v3-features.mdx
@@ -778,11 +778,11 @@ v3.0 introduces callable-based authorization for tools, resources, and prompts (
```python
from fastmcp import FastMCP
-from fastmcp.server.auth import require_auth, require_scopes
+from fastmcp.server.auth import require_scopes
mcp = FastMCP()
-@mcp.tool(auth=require_auth)
+@mcp.tool(auth=require_scopes("write"))
def protected_tool(): ...
@mcp.resource("data://secret", auth=require_scopes("read"))
@@ -796,10 +796,10 @@ def admin_prompt(): ...
```python
from fastmcp.server.middleware import AuthMiddleware
-from fastmcp.server.auth import require_auth, restrict_tag
+from fastmcp.server.auth import require_scopes, restrict_tag
-# Require auth for all components
-mcp = FastMCP(middleware=[AuthMiddleware(auth=require_auth)])
+# Require specific scope for all components
+mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes("api"))])
# Tag-based restrictions
mcp = FastMCP(middleware=[
@@ -808,7 +808,6 @@ mcp = FastMCP(middleware=[
```
Built-in checks:
-- `require_auth`: Requires any valid token
- `require_scopes(*scopes)`: Requires specific OAuth scopes
- `restrict_tag(tag, scopes)`: Requires scopes only for tagged components
diff --git a/docs/servers/authorization.mdx b/docs/servers/authorization.mdx
index ac65f21cb8..0ad0d569ee 100644
--- a/docs/servers/authorization.mdx
+++ b/docs/servers/authorization.mdx
@@ -18,6 +18,10 @@ The authorization model centers on a simple concept: callable functions that rec
Authorization relies on OAuth tokens which are only available with HTTP transports (SSE, Streamable HTTP). In STDIO mode, there's no OAuth mechanism, so `get_access_token()` returns `None` and all auth checks are skipped.
+
+When an `AuthProvider` is configured, all requests to the MCP endpoint must carry a valid token—unauthenticated requests are rejected at the transport level before any auth checks run. Authorization checks therefore differentiate between authenticated users based on their scopes and claims, not between authenticated and unauthenticated users.
+
+
## Auth Checks
An auth check is any callable that accepts an `AuthContext` and returns a boolean. The `AuthContext` provides access to the current token (if any) and the component being accessed.
@@ -31,27 +35,11 @@ def my_custom_check(ctx: AuthContext) -> bool:
return ctx.token is not None and "special" in ctx.token.scopes
```
-FastMCP provides three built-in auth checks that cover common authorization patterns.
-
-### require_auth
-
-The simplest check verifies that any valid authentication token is present. Unauthenticated requests are denied.
-
-```python
-from fastmcp import FastMCP
-from fastmcp.server.auth import require_auth
-
-mcp = FastMCP("Protected Server")
-
-@mcp.tool(auth=require_auth)
-def protected_operation() -> str:
- """Only accessible to authenticated users."""
- return "Success"
-```
+FastMCP provides two built-in auth checks that cover common authorization patterns.
### require_scopes
-For scope-based authorization, `require_scopes` checks that the token contains all specified OAuth scopes. When multiple scopes are provided, all must be present (AND logic).
+Scope-based authorization checks that the token contains all specified OAuth scopes. When multiple scopes are provided, all must be present (AND logic).
```python
from fastmcp import FastMCP
@@ -103,13 +91,13 @@ Multiple auth checks can be combined by passing a list. All checks must pass for
```python
from fastmcp import FastMCP
-from fastmcp.server.auth import require_auth, require_scopes
+from fastmcp.server.auth import require_scopes
mcp = FastMCP("Combined Auth Server")
-@mcp.tool(auth=[require_auth, require_scopes("admin")])
+@mcp.tool(auth=[require_scopes("admin"), require_scopes("write")])
def secure_admin_action() -> str:
- """Requires authentication AND the 'admin' scope."""
+ """Requires both 'admin' AND 'write' scopes."""
return "Secure admin action"
```
@@ -169,18 +157,18 @@ def require_verified_email(ctx: AuthContext) -> bool:
## Component-Level Authorization
-The `auth` parameter on decorators controls visibility of individual components. When auth checks fail for the current request, the component is hidden from list responses—it simply doesn't appear.
+The `auth` parameter on decorators controls visibility and access for individual components. When auth checks fail for the current request, the component is hidden from list responses and direct access returns not-found.
```python
from fastmcp import FastMCP
-from fastmcp.server.auth import require_auth, require_scopes
+from fastmcp.server.auth import require_scopes
mcp = FastMCP("Component Auth Server")
-@mcp.tool(auth=require_auth)
-def authenticated_tool() -> str:
- """Only visible to authenticated users."""
- return "Authenticated"
+@mcp.tool(auth=require_scopes("write"))
+def write_tool() -> str:
+ """Only visible to users with 'write' scope."""
+ return "Written"
@mcp.resource("secret://data", auth=require_scopes("read"))
def secret_resource() -> str:
@@ -193,38 +181,59 @@ def admin_prompt() -> str:
return "Admin prompt content"
```
-
-Component-level `auth` only controls visibility in list operations. It does not block direct access. Use `AuthMiddleware` to enforce authorization on execution.
-
+
+Component-level `auth` controls both visibility (list filtering) and access (direct lookups return not-found for unauthorized requests). Additionally use `AuthMiddleware` to apply server-wide authorization rules and get explicit `AuthorizationError` responses on unauthorized execution attempts.
+
## Server-Level Authorization
-For server-wide authorization enforcement, use `AuthMiddleware`. This middleware applies auth checks globally to all components—filtering list responses and blocking unauthorized execution.
+For server-wide authorization enforcement, use `AuthMiddleware`. This middleware applies auth checks globally to all components—filtering list responses and blocking unauthorized execution with explicit `AuthorizationError` responses.
```python
from fastmcp import FastMCP
-from fastmcp.server.auth import require_auth
+from fastmcp.server.auth import require_scopes
from fastmcp.server.middleware import AuthMiddleware
mcp = FastMCP(
"Enforced Auth Server",
- middleware=[AuthMiddleware(auth=require_auth)]
+ middleware=[AuthMiddleware(auth=require_scopes("api"))]
)
@mcp.tool
def any_tool() -> str:
- """Requires authentication to see AND call."""
+ """Requires 'api' scope to see AND call."""
return "Protected"
```
-### Filtering vs Enforcement
+### Component Auth + Middleware
+
+Component-level `auth` and `AuthMiddleware` work together as complementary layers. The middleware applies server-wide rules to all components, while component-level auth adds per-component requirements. Both layers are checked—all checks must pass.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_scopes, restrict_tag
+from fastmcp.server.middleware import AuthMiddleware
+
+mcp = FastMCP(
+ "Layered Auth Server",
+ middleware=[
+ AuthMiddleware(auth=restrict_tag("admin", scopes=["admin"]))
+ ]
+)
-| Behavior | Component-level `auth` | `AuthMiddleware` |
-|----------|------------------------|------------------|
-| Filters list responses | Yes | Yes |
-| Blocks execution | No | Yes (raises `AuthorizationError`) |
+# Requires "write" scope (component-level)
+# Also requires "admin" scope if tagged "admin" (middleware-level)
+@mcp.tool(auth=require_scopes("write"), tags={"admin"})
+def admin_write() -> str:
+ """Requires both 'write' AND 'admin' scopes."""
+ return "Admin write"
-Component-level auth is useful for hiding components from unauthorized users while still allowing advanced clients to access them directly. `AuthMiddleware` provides complete enforcement by raising `AuthorizationError` when unauthorized requests attempt execution.
+# Requires "write" scope (component-level only)
+@mcp.tool(auth=require_scopes("write"))
+def user_write() -> str:
+ """Requires 'write' scope."""
+ return "User write"
+```
### Tag-Based Global Authorization
@@ -338,7 +347,6 @@ from fastmcp.server.auth import (
AccessToken, # Token with .token, .client_id, .scopes, .expires_at, .claims
AuthContext, # Context with .token, .component
AuthCheck, # Type alias: Callable[[AuthContext], bool]
- require_auth, # Built-in: requires any valid token
require_scopes, # Built-in: requires specific scopes
restrict_tag, # Built-in: tag-based scope requirements
run_auth_checks, # Utility: run checks with AND logic
diff --git a/src/fastmcp/server/auth/__init__.py b/src/fastmcp/server/auth/__init__.py
index d8d221a3d2..94e23dca61 100644
--- a/src/fastmcp/server/auth/__init__.py
+++ b/src/fastmcp/server/auth/__init__.py
@@ -8,7 +8,6 @@
from .authorization import (
AuthCheck,
AuthContext,
- require_auth,
require_scopes,
restrict_tag,
run_auth_checks,
@@ -32,7 +31,6 @@
"RemoteAuthProvider",
"StaticTokenVerifier",
"TokenVerifier",
- "require_auth",
"require_scopes",
"restrict_tag",
"run_auth_checks",
diff --git a/src/fastmcp/server/auth/authorization.py b/src/fastmcp/server/auth/authorization.py
index ae9e64a5b1..dd0e16cc17 100644
--- a/src/fastmcp/server/auth/authorization.py
+++ b/src/fastmcp/server/auth/authorization.py
@@ -11,17 +11,17 @@
Example:
```python
from fastmcp import FastMCP
- from fastmcp.server.auth import require_auth, require_scopes
+ from fastmcp.server.auth import require_scopes
mcp = FastMCP()
- @mcp.tool(auth=require_auth)
+ @mcp.tool(auth=require_scopes("write"))
def protected_tool(): ...
@mcp.resource("data://secret", auth=require_scopes("read"))
def secret_data(): ...
- @mcp.prompt(auth=require_auth)
+ @mcp.prompt(auth=require_scopes("admin"))
def admin_prompt(): ...
```
"""
@@ -74,20 +74,6 @@ def tool(self) -> Tool | None:
AuthCheck = Callable[[AuthContext], bool]
-def require_auth(ctx: AuthContext) -> bool:
- """Require any valid authentication.
-
- Returns True if the request has a valid token, False otherwise.
-
- Example:
- ```python
- @mcp.tool(auth=require_auth)
- def protected_tool(): ...
- ```
- """
- return ctx.token is not None
-
-
def require_scopes(*scopes: str) -> AuthCheck:
"""Require specific OAuth scopes.
diff --git a/src/fastmcp/server/middleware/authorization.py b/src/fastmcp/server/middleware/authorization.py
index 46038f6c93..6a50ed656f 100644
--- a/src/fastmcp/server/middleware/authorization.py
+++ b/src/fastmcp/server/middleware/authorization.py
@@ -6,12 +6,12 @@
Example:
```python
from fastmcp import FastMCP
- from fastmcp.server.auth import require_auth, require_scopes, restrict_tag
+ from fastmcp.server.auth import require_scopes, restrict_tag
from fastmcp.server.middleware import AuthMiddleware
- # Require auth for all components
+ # Require specific scope for all components
mcp = FastMCP(middleware=[
- AuthMiddleware(auth=require_auth)
+ AuthMiddleware(auth=require_scopes("api"))
])
# Tag-based: components tagged "admin" require "admin" scope
@@ -67,17 +67,14 @@ class AuthMiddleware(Middleware):
Example:
```python
from fastmcp import FastMCP
- from fastmcp.server.auth import require_auth, require_scopes
-
- # Require any authentication for all components
- mcp = FastMCP(middleware=[AuthMiddleware(auth=require_auth)])
+ from fastmcp.server.auth import require_scopes
# Require specific scope for all components
mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes("api"))])
- # Combined checks (AND logic)
+ # Multiple scopes (AND logic)
mcp = FastMCP(middleware=[
- AuthMiddleware(auth=[require_auth, require_scopes("api")])
+ AuthMiddleware(auth=require_scopes("read", "api"))
])
```
"""
diff --git a/tests/server/auth/test_authorization.py b/tests/server/auth/test_authorization.py
index 6eaaede321..6bab4cecdf 100644
--- a/tests/server/auth/test_authorization.py
+++ b/tests/server/auth/test_authorization.py
@@ -12,7 +12,6 @@
from fastmcp.server.auth import (
AccessToken,
AuthContext,
- require_auth,
require_scopes,
restrict_tag,
run_auth_checks,
@@ -42,21 +41,6 @@ def make_tool() -> Mock:
return tool
-# =============================================================================
-# Tests for require_auth
-# =============================================================================
-
-
-class TestRequireAuth:
- def test_returns_true_with_token(self):
- ctx = AuthContext(token=make_token(), component=make_tool())
- assert require_auth(ctx) is True
-
- def test_returns_false_without_token(self):
- ctx = AuthContext(token=None, component=make_tool())
- assert require_auth(ctx) is False
-
-
# =============================================================================
# Tests for require_scopes
# =============================================================================
@@ -137,23 +121,23 @@ def test_allows_access_when_tag_present_with_scope(self):
class TestRunAuthChecks:
def test_single_check_passes(self):
- ctx = AuthContext(token=make_token(), component=make_tool())
- assert run_auth_checks(require_auth, ctx) is True
+ ctx = AuthContext(token=make_token(scopes=["test"]), component=make_tool())
+ assert run_auth_checks(require_scopes("test"), ctx) is True
def test_single_check_fails(self):
ctx = AuthContext(token=None, component=make_tool())
- assert run_auth_checks(require_auth, ctx) is False
+ assert run_auth_checks(require_scopes("test"), ctx) is False
def test_multiple_checks_all_pass(self):
- token = make_token(scopes=["admin"])
+ token = make_token(scopes=["test", "admin"])
ctx = AuthContext(token=token, component=make_tool())
- checks = [require_auth, require_scopes("admin")]
+ checks = [require_scopes("test"), require_scopes("admin")]
assert run_auth_checks(checks, ctx) is True
def test_multiple_checks_one_fails(self):
token = make_token(scopes=["read"])
ctx = AuthContext(token=token, component=make_tool())
- checks = [require_auth, require_scopes("admin")]
+ checks = [require_scopes("read"), require_scopes("admin")]
assert run_auth_checks(checks, ctx) is False
def test_empty_list_passes(self):
@@ -244,7 +228,7 @@ def public_tool() -> str:
async def test_tool_with_auth_hidden_without_token(self):
mcp = FastMCP()
- @mcp.tool(auth=require_auth)
+ @mcp.tool(auth=require_scopes("test"))
def protected_tool() -> str:
return "protected"
@@ -255,12 +239,12 @@ def protected_tool() -> str:
async def test_tool_with_auth_visible_with_token(self):
mcp = FastMCP()
- @mcp.tool(auth=require_auth)
+ @mcp.tool(auth=require_scopes("test"))
def protected_tool() -> str:
return "protected"
# Set token in context
- token = make_token()
+ token = make_token(scopes=["test"])
tok = set_token(token)
try:
tools = await mcp.list_tools()
@@ -306,7 +290,7 @@ async def test_get_tool_returns_none_without_auth(self):
"""get_tool() returns None for unauthorized tools (consistent with list filtering)."""
mcp = FastMCP()
- @mcp.tool(auth=require_auth)
+ @mcp.tool(auth=require_scopes("test"))
def protected_tool() -> str:
return "protected"
@@ -317,11 +301,11 @@ def protected_tool() -> str:
async def test_get_tool_returns_tool_with_auth(self):
mcp = FastMCP()
- @mcp.tool(auth=require_auth)
+ @mcp.tool(auth=require_scopes("test"))
def protected_tool() -> str:
return "protected"
- token = make_token()
+ token = make_token(scopes=["test"])
tok = set_token(token)
try:
tool = await mcp.get_tool("protected_tool")
@@ -344,7 +328,7 @@ class TestAuthMiddleware:
"""
async def test_middleware_filters_tools_without_token(self):
- mcp = FastMCP(middleware=[AuthMiddleware(auth=require_auth)])
+ mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes("test"))])
@mcp.tool
def public_tool() -> str:
@@ -355,13 +339,13 @@ def public_tool() -> str:
assert len(result.tools) == 0
async def test_middleware_allows_tools_with_token(self):
- mcp = FastMCP(middleware=[AuthMiddleware(auth=require_auth)])
+ mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes("test"))])
@mcp.tool
def public_tool() -> str:
return "public"
- token = make_token()
+ token = make_token(scopes=["test"])
tok = set_token(token)
try:
result = await mcp._list_tools_mcp(mcp_types.ListToolsRequest())
@@ -435,7 +419,7 @@ async def test_client_only_sees_authorized_tools(self):
def public_tool() -> str:
return "public"
- @mcp.tool(auth=require_auth)
+ @mcp.tool(auth=require_scopes("test"))
def protected_tool() -> str:
return "protected"
@@ -452,12 +436,12 @@ async def test_client_with_token_sees_all_authorized_tools(self):
def public_tool() -> str:
return "public"
- @mcp.tool(auth=require_auth)
+ @mcp.tool(auth=require_scopes("test"))
def protected_tool() -> str:
return "protected"
# Set token before creating client
- token = make_token()
+ token = make_token(scopes=["test"])
tok = set_token(token)
try:
async with Client(mcp) as client:
@@ -482,7 +466,7 @@ async def test_transformed_tool_preserves_auth(self):
mcp = FastMCP()
- @mcp.tool(auth=require_auth)
+ @mcp.tool(auth=require_scopes("test"))
def protected_tool(x: int) -> str:
return str(x)
@@ -507,7 +491,7 @@ async def test_transformed_tool_filtered_without_token(self):
mcp = FastMCP()
- @mcp.tool(auth=require_auth)
+ @mcp.tool(auth=require_scopes("test"))
def protected_tool(x: int) -> str:
return str(x)
@@ -526,7 +510,7 @@ async def test_transformed_tool_visible_with_token(self):
mcp = FastMCP()
- @mcp.tool(auth=require_auth)
+ @mcp.tool(auth=require_scopes("test"))
def protected_tool(x: int) -> str:
return str(x)
@@ -536,7 +520,7 @@ def protected_tool(x: int) -> str:
)
# With token, transformed tool should be visible
- token = make_token()
+ token = make_token(scopes=["test"])
tok = set_token(token)
try:
tools = await mcp.list_tools()
@@ -555,7 +539,7 @@ class TestAuthMiddlewareCallTool:
async def test_middleware_blocks_call_without_auth(self):
"""AuthMiddleware should raise AuthorizationError on unauthorized call."""
- mcp = FastMCP(middleware=[AuthMiddleware(auth=require_auth)])
+ mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes("test"))])
@mcp.tool
def my_tool() -> str:
@@ -573,14 +557,14 @@ def my_tool() -> str:
async def test_middleware_allows_call_with_auth(self):
"""AuthMiddleware should allow tool call with valid token."""
- mcp = FastMCP(middleware=[AuthMiddleware(auth=require_auth)])
+ mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes("test"))])
@mcp.tool
def my_tool() -> str:
return "result"
# With token, calling the tool should succeed
- token = make_token()
+ token = make_token(scopes=["test"])
tok = set_token(token)
try:
async with Client(mcp) as client: