Skip to content
Merged
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
11 changes: 5 additions & 6 deletions docs/development/v3-notes/v3-features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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=[
Expand All @@ -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

Expand Down
90 changes: 49 additions & 41 deletions docs/servers/authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Note>

<Note>
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.
</Note>

## 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.
Expand All @@ -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
Expand Down Expand Up @@ -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"
```

Expand Down Expand Up @@ -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:
Expand All @@ -193,38 +181,59 @@ def admin_prompt() -> str:
return "Admin prompt content"
```

<Warning>
Component-level `auth` only controls visibility in list operations. It does not block direct access. Use `AuthMiddleware` to enforce authorization on execution.
</Warning>
<Note>
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.
</Note>

## 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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions src/fastmcp/server/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from .authorization import (
AuthCheck,
AuthContext,
require_auth,
require_scopes,
restrict_tag,
run_auth_checks,
Expand All @@ -32,7 +31,6 @@
"RemoteAuthProvider",
"StaticTokenVerifier",
"TokenVerifier",
"require_auth",
"require_scopes",
"restrict_tag",
"run_auth_checks",
Expand Down
20 changes: 3 additions & 17 deletions src/fastmcp/server/auth/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(): ...
```
"""
Expand Down Expand Up @@ -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.

Expand Down
15 changes: 6 additions & 9 deletions src/fastmcp/server/middleware/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))
])
```
"""
Expand Down
Loading
Loading