mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
Support async auth checks (#3152)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
This commit is contained in:
parent
25e2f4da32
commit
50b23299f8
21 changed files with 292 additions and 118 deletions
|
|
@ -96,7 +96,7 @@ When modifying MCP functionality, changes typically need to be applied across al
|
|||
|
||||
- Uses Mintlify framework
|
||||
- Files must be in docs.json to be included
|
||||
- Never modify `docs/python-sdk/**` (auto-generated)
|
||||
- Do not manually modify `docs/python-sdk/**` — a bot automatically updates these files via commits added to PRs
|
||||
- **Core Principle:** A feature doesn't exist unless it is documented!
|
||||
|
||||
### Documentation Guidelines
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ async def get_emails(
|
|||
|
||||
Background tasks now use a distributed Redis notification queue for reliable delivery ([#2906](https://github.com/jlowin/fastmcp/pull/2906)). Elicitation switches from polling to BLPOP (single blocking call instead of ~7,200 round-trips/hour), and notification delivery retries up to 3x with TTL-based expiration.
|
||||
|
||||
### Async Auth Checks
|
||||
|
||||
Auth check functions can now be `async`, enabling authorization decisions that depend on asynchronous operations like reading server state via `Context.get_state` or calling external services ([#3150](https://github.com/jlowin/fastmcp/issues/3150)). Sync and async checks can be freely mixed. Previously, passing an async function as an auth check would silently pass (coroutine objects are truthy).
|
||||
|
||||
### Breaking: Deprecated `FastMCP()` Constructor Kwargs Removed
|
||||
|
||||
Sixteen deprecated keyword arguments have been removed from `FastMCP.__init__`. Passing any of them now raises `TypeError` with a migration hint. Environment variables (e.g., `FASTMCP_HOST`) continue to work — only the constructor kwargs moved.
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ A template for dynamically creating resources.
|
|||
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/template.py#L130" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_function(fn: Callable[..., Any], uri_template: str, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, mime_type: str | None = None, tags: set[str] | None = None, annotations: Annotations | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheckCallable | list[AuthCheckCallable] | None = None) -> FunctionResourceTemplate
|
||||
from_function(fn: Callable[..., Any], uri_template: str, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, mime_type: str | None = None, tags: set[str] | None = None, annotations: Annotations | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheck | list[AuthCheck] | None = None) -> FunctionResourceTemplate
|
||||
```
|
||||
|
||||
#### `set_default_mime_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/template.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
|
@ -237,7 +237,7 @@ FunctionResourceTemplate splats the params dict since .fn expects **kwargs.
|
|||
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/template.py#L460" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_function(cls, fn: Callable[..., Any], uri_template: str, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, mime_type: str | None = None, tags: set[str] | None = None, annotations: Annotations | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheckCallable | list[AuthCheckCallable] | None = None) -> FunctionResourceTemplate
|
||||
from_function(cls, fn: Callable[..., Any], uri_template: str, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, mime_type: str | None = None, tags: set[str] | None = None, annotations: Annotations | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheck | list[AuthCheck] | None = None) -> FunctionResourceTemplate
|
||||
```
|
||||
|
||||
Create a template from a function.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ Example:
|
|||
|
||||
## Functions
|
||||
|
||||
### `require_scopes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L77" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `require_scopes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L78" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
require_scopes(*scopes: str) -> AuthCheck
|
||||
|
|
@ -52,7 +52,7 @@ in the token (AND logic).
|
|||
- `*scopes`: One or more scope strings that must all be present.
|
||||
|
||||
|
||||
### `restrict_tag` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L105" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `restrict_tag` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
restrict_tag(tag: str) -> AuthCheck
|
||||
|
|
@ -69,7 +69,7 @@ required scopes. If the component doesn't have the tag, access is allowed.
|
|||
- `scopes`: List of scopes required when the tag is present.
|
||||
|
||||
|
||||
### `run_auth_checks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L133" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `run_auth_checks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run_auth_checks(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> bool
|
||||
|
|
@ -78,7 +78,8 @@ run_auth_checks(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> bool
|
|||
|
||||
Run auth checks with AND logic.
|
||||
|
||||
All checks must pass for authorization to succeed.
|
||||
All checks must pass for authorization to succeed. Checks can be
|
||||
synchronous or asynchronous functions.
|
||||
|
||||
Auth checks can:
|
||||
- Return True to allow access
|
||||
|
|
@ -88,6 +89,7 @@ Auth checks can:
|
|||
|
||||
**Args:**
|
||||
- `checks`: A single check function or list of check functions.
|
||||
Each check can be sync (returns bool) or async (returns Awaitable[bool]).
|
||||
- `ctx`: The auth context to pass to each check.
|
||||
|
||||
**Returns:**
|
||||
|
|
@ -99,7 +101,7 @@ Auth checks can:
|
|||
|
||||
## Classes
|
||||
|
||||
### `AuthContext` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L47" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `AuthContext` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L48" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Context passed to auth check callables.
|
||||
|
|
@ -115,7 +117,7 @@ access to the current authentication token and the component being accessed.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L64" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self) -> Tool | None
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ registration functionality to LocalProvider.
|
|||
|
||||
## Classes
|
||||
|
||||
### `ToolDecoratorMixin` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L31" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ToolDecoratorMixin` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L32" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Mixin class providing tool decorator functionality for LocalProvider.
|
||||
|
|
@ -26,7 +26,7 @@ This mixin contains all methods related to:
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L39" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L40" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_tool(self: LocalProvider, tool: Tool | Callable[..., Any]) -> Tool
|
||||
|
|
@ -37,19 +37,19 @@ Add a tool to this provider's storage.
|
|||
Accepts either a Tool object or a decorated function with __fastmcp__ metadata.
|
||||
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L90" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L91" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self: LocalProvider, name_or_fn: AnyFunction) -> FunctionTool
|
||||
```
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L112" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L113" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self: LocalProvider, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool]
|
||||
```
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L137" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L138" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self: LocalProvider, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sidebarTitle: tool
|
|||
|
||||
## Functions
|
||||
|
||||
### `default_serializer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `default_serializer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L56" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
default_serializer(data: Any) -> str
|
||||
|
|
@ -15,17 +15,17 @@ default_serializer(data: Any) -> str
|
|||
|
||||
## Classes
|
||||
|
||||
### `ToolResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ToolResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `to_mcp_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L108" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `to_mcp_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L105" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_mcp_result(self) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult
|
||||
```
|
||||
|
||||
### `Tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L124" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `Tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L121" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Internal tool registration info.
|
||||
|
|
@ -33,7 +33,7 @@ Internal tool registration info.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `to_mcp_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L166" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `to_mcp_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_mcp_tool(self, **overrides: Any) -> MCPTool
|
||||
|
|
@ -42,7 +42,7 @@ to_mcp_tool(self, **overrides: Any) -> MCPTool
|
|||
Convert the FastMCP tool to an MCP tool.
|
||||
|
||||
|
||||
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L193" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L190" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_function(cls, fn: Callable[..., Any]) -> FunctionTool
|
||||
|
|
@ -51,7 +51,7 @@ from_function(cls, fn: Callable[..., Any]) -> FunctionTool
|
|||
Create a Tool from a function.
|
||||
|
||||
|
||||
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L233" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L230" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run(self, arguments: dict[str, Any]) -> ToolResult
|
||||
|
|
@ -66,7 +66,7 @@ implemented by subclasses.
|
|||
(list of ContentBlocks, dict of structured output).
|
||||
|
||||
|
||||
#### `convert_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L245" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `convert_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
convert_result(self, raw_value: Any) -> ToolResult
|
||||
|
|
@ -78,7 +78,7 @@ Handles ToolResult passthrough and converts raw values using the tool's
|
|||
attributes (serializer, output_schema) for proper conversion.
|
||||
|
||||
|
||||
#### `register_with_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `register_with_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L334" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
register_with_docket(self, docket: Docket) -> None
|
||||
|
|
@ -87,7 +87,7 @@ register_with_docket(self, docket: Docket) -> None
|
|||
Register this tool with docket for background execution.
|
||||
|
||||
|
||||
#### `add_to_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L343" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `add_to_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L340" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_to_docket(self, docket: Docket, arguments: dict[str, Any], **kwargs: Any) -> Execution
|
||||
|
|
@ -103,13 +103,13 @@ Schedule this tool for background execution via docket.
|
|||
- `**kwargs`: Additional kwargs passed to docket.add()
|
||||
|
||||
|
||||
#### `from_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L367" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `from_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L364" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_tool(cls, tool: Tool) -> TransformedTool
|
||||
```
|
||||
|
||||
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L398" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L395" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_span_attributes(self) -> dict[str, Any]
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ When an `AuthProvider` is configured, all requests to the MCP endpoint must carr
|
|||
|
||||
## 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.
|
||||
An auth check is any callable that accepts an `AuthContext` and returns a boolean. Auth checks can be synchronous or asynchronous, so checks that need to perform async operations (like reading server state or calling external services) work naturally.
|
||||
|
||||
```python
|
||||
from fastmcp.server.auth import AuthContext
|
||||
|
|
@ -137,6 +137,34 @@ def advanced_feature() -> str:
|
|||
return "Advanced feature"
|
||||
```
|
||||
|
||||
### Async Auth Checks
|
||||
|
||||
Auth checks can be `async` functions, which is useful when the authorization decision depends on asynchronous operations like reading server state or querying external services.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import AuthContext
|
||||
|
||||
mcp = FastMCP("Async Auth Server")
|
||||
|
||||
async def check_user_permissions(ctx: AuthContext) -> bool:
|
||||
"""Async auth check that reads server state."""
|
||||
if ctx.token is None:
|
||||
return False
|
||||
user_id = ctx.token.claims.get("sub")
|
||||
# Async operations work naturally in auth checks
|
||||
permissions = await fetch_user_permissions(user_id)
|
||||
return "admin" in permissions
|
||||
|
||||
@mcp.tool(auth=check_user_permissions)
|
||||
def admin_tool() -> str:
|
||||
return "Admin action completed"
|
||||
```
|
||||
|
||||
Sync and async checks can be freely combined in a list — each check is handled according to its type.
|
||||
|
||||
### Error Handling
|
||||
|
||||
Auth checks can raise exceptions for explicit denial with custom messages:
|
||||
|
||||
- **`AuthorizationError`**: Propagates with its custom message, useful for explaining why access was denied
|
||||
|
|
@ -346,7 +374,7 @@ def require_matching_tag(ctx: AuthContext) -> bool:
|
|||
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]
|
||||
AuthCheck, # Type alias: sync or async Callable[[AuthContext], bool]
|
||||
require_scopes, # Built-in: requires specific scopes
|
||||
restrict_tag, # Built-in: tag-based scope requirements
|
||||
run_auth_checks, # Utility: run checks with AND logic
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ import fastmcp
|
|||
from fastmcp.decorators import resolve_task_config
|
||||
from fastmcp.exceptions import PromptError
|
||||
from fastmcp.prompts.prompt import Prompt, PromptArgument, PromptResult
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.dependencies import (
|
||||
transform_context_annotations,
|
||||
without_injected_parameters,
|
||||
)
|
||||
from fastmcp.server.tasks.config import TaskConfig
|
||||
from fastmcp.tools.tool import AuthCheckCallable
|
||||
from fastmcp.utilities.async_utils import call_sync_fn_in_threadpool
|
||||
from fastmcp.utilities.json_schema import compress_schema
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
|
@ -67,7 +67,7 @@ class PromptMeta:
|
|||
tags: set[str] | None = None
|
||||
meta: dict[str, Any] | None = None
|
||||
task: bool | TaskConfig | None = None
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None
|
||||
auth: AuthCheck | list[AuthCheck] | None = None
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ class FunctionPrompt(Prompt):
|
|||
tags: set[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionPrompt:
|
||||
"""Create a Prompt from a function.
|
||||
|
||||
|
|
@ -377,7 +377,7 @@ def prompt(
|
|||
tags: set[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[F], F]: ...
|
||||
@overload
|
||||
def prompt(
|
||||
|
|
@ -391,7 +391,7 @@ def prompt(
|
|||
tags: set[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[F], F]: ...
|
||||
|
||||
|
||||
|
|
@ -406,7 +406,7 @@ def prompt(
|
|||
tags: set[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Any:
|
||||
"""Standalone decorator to mark a function as an MCP prompt.
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ from mcp.types import PromptArgument as SDKPromptArgument
|
|||
from pydantic import Field
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
|
||||
from fastmcp.tools.tool import AuthCheckCallable
|
||||
from fastmcp.utilities.components import FastMCPComponent
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import (
|
||||
|
|
@ -195,7 +195,7 @@ class Prompt(FastMCPComponent):
|
|||
arguments: list[PromptArgument] | None = Field(
|
||||
default=None, description="Arguments that can be passed to the prompt"
|
||||
)
|
||||
auth: SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None] = Field(
|
||||
auth: SkipJsonSchema[AuthCheck | list[AuthCheck] | None] = Field(
|
||||
default=None, description="Authorization checks for this prompt", exclude=True
|
||||
)
|
||||
|
||||
|
|
@ -237,7 +237,7 @@ class Prompt(FastMCPComponent):
|
|||
tags: set[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionPrompt:
|
||||
"""Create a Prompt from a function.
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ import fastmcp
|
|||
from fastmcp.decorators import resolve_task_config
|
||||
from fastmcp.resources.resource import Resource, ResourceResult
|
||||
from fastmcp.server.apps import resolve_ui_mime_type
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.dependencies import (
|
||||
transform_context_annotations,
|
||||
without_injected_parameters,
|
||||
)
|
||||
from fastmcp.server.tasks.config import TaskConfig
|
||||
from fastmcp.tools.tool import AuthCheckCallable
|
||||
from fastmcp.utilities.async_utils import call_sync_fn_in_threadpool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -57,7 +57,7 @@ class ResourceMeta:
|
|||
annotations: Annotations | None = None
|
||||
meta: dict[str, Any] | None = None
|
||||
task: bool | TaskConfig | None = None
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None
|
||||
auth: AuthCheck | list[AuthCheck] | None = None
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ class FunctionResource(Resource):
|
|||
annotations: Annotations | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionResource:
|
||||
"""Create a FunctionResource from a function.
|
||||
|
||||
|
|
@ -246,7 +246,7 @@ def resource(
|
|||
annotations: Annotations | dict[str, Any] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[F], F]:
|
||||
"""Standalone decorator to mark a function as an MCP resource.
|
||||
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ from pydantic import (
|
|||
from pydantic.json_schema import SkipJsonSchema
|
||||
from typing_extensions import Self
|
||||
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
|
||||
from fastmcp.tools.tool import AuthCheckCallable
|
||||
from fastmcp.utilities.components import FastMCPComponent
|
||||
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ class Resource(FastMCPComponent):
|
|||
Field(description="Optional annotations about the resource's behavior"),
|
||||
] = None
|
||||
auth: Annotated[
|
||||
SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None],
|
||||
SkipJsonSchema[AuthCheck | list[AuthCheck] | None],
|
||||
Field(description="Authorization checks for this resource", exclude=True),
|
||||
] = None
|
||||
|
||||
|
|
@ -247,7 +247,7 @@ class Resource(FastMCPComponent):
|
|||
annotations: Annotations | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionResource:
|
||||
from fastmcp.resources.function_resource import (
|
||||
FunctionResource,
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ from pydantic import (
|
|||
|
||||
from fastmcp.resources.resource import Resource, ResourceResult
|
||||
from fastmcp.server.apps import resolve_ui_mime_type
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.dependencies import (
|
||||
transform_context_annotations,
|
||||
without_injected_parameters,
|
||||
)
|
||||
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
|
||||
from fastmcp.tools.tool import AuthCheckCallable
|
||||
from fastmcp.utilities.components import FastMCPComponent
|
||||
from fastmcp.utilities.json_schema import compress_schema
|
||||
from fastmcp.utilities.types import get_cached_typeadapter
|
||||
|
|
@ -117,7 +117,7 @@ class ResourceTemplate(FastMCPComponent):
|
|||
annotations: Annotations | None = Field(
|
||||
default=None, description="Optional annotations about the resource's behavior"
|
||||
)
|
||||
auth: SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None] = Field(
|
||||
auth: SkipJsonSchema[AuthCheck | list[AuthCheck] | None] = Field(
|
||||
default=None,
|
||||
description="Authorization checks for this resource template",
|
||||
exclude=True,
|
||||
|
|
@ -140,7 +140,7 @@ class ResourceTemplate(FastMCPComponent):
|
|||
annotations: Annotations | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionResourceTemplate:
|
||||
return FunctionResourceTemplate.from_function(
|
||||
fn=fn,
|
||||
|
|
@ -471,7 +471,7 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|||
annotations: Annotations | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionResourceTemplate:
|
||||
"""Create a template from a function."""
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,9 @@ Example:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
|
|
@ -70,8 +71,8 @@ class AuthContext:
|
|||
return self.component if isinstance(self.component, Tool) else None
|
||||
|
||||
|
||||
# Type alias for auth check functions
|
||||
AuthCheck = Callable[[AuthContext], bool]
|
||||
# Type alias for auth check functions (sync or async)
|
||||
AuthCheck = Callable[[AuthContext], bool] | Callable[[AuthContext], Awaitable[bool]]
|
||||
|
||||
|
||||
def require_scopes(*scopes: str) -> AuthCheck:
|
||||
|
|
@ -130,13 +131,14 @@ def restrict_tag(tag: str, *, scopes: list[str]) -> AuthCheck:
|
|||
return check
|
||||
|
||||
|
||||
def run_auth_checks(
|
||||
async def run_auth_checks(
|
||||
checks: AuthCheck | list[AuthCheck],
|
||||
ctx: AuthContext,
|
||||
) -> bool:
|
||||
"""Run auth checks with AND logic.
|
||||
|
||||
All checks must pass for authorization to succeed.
|
||||
All checks must pass for authorization to succeed. Checks can be
|
||||
synchronous or asynchronous functions.
|
||||
|
||||
Auth checks can:
|
||||
- Return True to allow access
|
||||
|
|
@ -146,6 +148,7 @@ def run_auth_checks(
|
|||
|
||||
Args:
|
||||
checks: A single check function or list of check functions.
|
||||
Each check can be sync (returns bool) or async (returns Awaitable[bool]).
|
||||
ctx: The auth context to pass to each check.
|
||||
|
||||
Returns:
|
||||
|
|
@ -159,7 +162,10 @@ def run_auth_checks(
|
|||
|
||||
for check in check_list:
|
||||
try:
|
||||
if not check(ctx):
|
||||
result = check(ctx)
|
||||
if inspect.isawaitable(result):
|
||||
result = await result
|
||||
if not result:
|
||||
return False
|
||||
except AuthorizationError:
|
||||
# Let AuthorizationError propagate with its custom message
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ class AuthMiddleware(Middleware):
|
|||
authorized_tools: list[Tool] = []
|
||||
for tool in tools:
|
||||
ctx = AuthContext(token=token, component=tool)
|
||||
if run_auth_checks(self.auth, ctx):
|
||||
if await run_auth_checks(self.auth, ctx):
|
||||
authorized_tools.append(tool)
|
||||
|
||||
return authorized_tools
|
||||
|
|
@ -143,7 +143,7 @@ class AuthMiddleware(Middleware):
|
|||
# Global auth check
|
||||
token = get_access_token()
|
||||
ctx = AuthContext(token=token, component=tool)
|
||||
if not run_auth_checks(self.auth, ctx):
|
||||
if not await run_auth_checks(self.auth, ctx):
|
||||
raise AuthorizationError(
|
||||
f"Authorization failed for tool '{tool_name}': insufficient permissions"
|
||||
)
|
||||
|
|
@ -169,7 +169,7 @@ class AuthMiddleware(Middleware):
|
|||
authorized_resources: list[Resource] = []
|
||||
for resource in resources:
|
||||
ctx = AuthContext(token=token, component=resource)
|
||||
if run_auth_checks(self.auth, ctx):
|
||||
if await run_auth_checks(self.auth, ctx):
|
||||
authorized_resources.append(resource)
|
||||
|
||||
return authorized_resources
|
||||
|
|
@ -210,7 +210,7 @@ class AuthMiddleware(Middleware):
|
|||
# Global auth check
|
||||
token = get_access_token()
|
||||
ctx = AuthContext(token=token, component=component)
|
||||
if not run_auth_checks(self.auth, ctx):
|
||||
if not await run_auth_checks(self.auth, ctx):
|
||||
raise AuthorizationError(
|
||||
f"Authorization failed for resource '{uri}': insufficient permissions"
|
||||
)
|
||||
|
|
@ -238,7 +238,7 @@ class AuthMiddleware(Middleware):
|
|||
authorized_templates: list[ResourceTemplate] = []
|
||||
for template in templates:
|
||||
ctx = AuthContext(token=token, component=template)
|
||||
if run_auth_checks(self.auth, ctx):
|
||||
if await run_auth_checks(self.auth, ctx):
|
||||
authorized_templates.append(template)
|
||||
|
||||
return authorized_templates
|
||||
|
|
@ -262,7 +262,7 @@ class AuthMiddleware(Middleware):
|
|||
authorized_prompts: list[Prompt] = []
|
||||
for prompt in prompts:
|
||||
ctx = AuthContext(token=token, component=prompt)
|
||||
if run_auth_checks(self.auth, ctx):
|
||||
if await run_auth_checks(self.auth, ctx):
|
||||
authorized_prompts.append(prompt)
|
||||
|
||||
return authorized_prompts
|
||||
|
|
@ -301,7 +301,7 @@ class AuthMiddleware(Middleware):
|
|||
# Global auth check
|
||||
token = get_access_token()
|
||||
ctx = AuthContext(token=token, component=prompt)
|
||||
if not run_auth_checks(self.auth, ctx):
|
||||
if not await run_auth_checks(self.auth, ctx):
|
||||
raise AuthorizationError(
|
||||
f"Authorization failed for prompt '{prompt_name}': insufficient permissions"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ from mcp.types import AnyFunction
|
|||
import fastmcp
|
||||
from fastmcp.prompts.function_prompt import FunctionPrompt
|
||||
from fastmcp.prompts.prompt import Prompt
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.tasks.config import TaskConfig
|
||||
from fastmcp.tools.tool import AuthCheckCallable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp.server.providers.local_provider import LocalProvider
|
||||
|
|
@ -82,7 +82,7 @@ class PromptDecoratorMixin:
|
|||
enabled: bool = True,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionPrompt: ...
|
||||
|
||||
@overload
|
||||
|
|
@ -99,7 +99,7 @@ class PromptDecoratorMixin:
|
|||
enabled: bool = True,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[AnyFunction], FunctionPrompt]: ...
|
||||
|
||||
def prompt(
|
||||
|
|
@ -115,7 +115,7 @@ class PromptDecoratorMixin:
|
|||
enabled: bool = True,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> (
|
||||
Callable[[AnyFunction], FunctionPrompt]
|
||||
| FunctionPrompt
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import fastmcp
|
|||
from fastmcp.resources.function_resource import resource as standalone_resource
|
||||
from fastmcp.resources.resource import Resource
|
||||
from fastmcp.resources.template import ResourceTemplate
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.tasks.config import TaskConfig
|
||||
from fastmcp.tools.tool import AuthCheckCallable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp.server.providers.local_provider import LocalProvider
|
||||
|
|
@ -117,7 +117,7 @@ class ResourceDecoratorMixin:
|
|||
annotations: Annotations | dict[str, Any] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction]:
|
||||
"""Decorator to register a function as a resource.
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@ import mcp.types
|
|||
from mcp.types import AnyFunction, ToolAnnotations
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.tasks.config import TaskConfig
|
||||
from fastmcp.tools.function_tool import FunctionTool
|
||||
from fastmcp.tools.tool import AuthCheckCallable, Tool
|
||||
from fastmcp.tools.tool import Tool
|
||||
from fastmcp.utilities.types import NotSet, NotSetT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -105,7 +106,7 @@ class ToolDecoratorMixin:
|
|||
task: bool | TaskConfig | None = None,
|
||||
serializer: ToolResultSerializerType | None = None, # Deprecated
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionTool: ...
|
||||
|
||||
@overload
|
||||
|
|
@ -127,7 +128,7 @@ class ToolDecoratorMixin:
|
|||
task: bool | TaskConfig | None = None,
|
||||
serializer: ToolResultSerializerType | None = None, # Deprecated
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[AnyFunction], FunctionTool]: ...
|
||||
|
||||
# NOTE: This method mirrors fastmcp.tools.tool() but adds registration,
|
||||
|
|
@ -152,7 +153,7 @@ class ToolDecoratorMixin:
|
|||
task: bool | TaskConfig | None = None,
|
||||
serializer: ToolResultSerializerType | None = None, # Deprecated
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> (
|
||||
Callable[[AnyFunction], FunctionTool]
|
||||
| FunctionTool
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ from fastmcp.server.apps import (
|
|||
app_config_to_meta_dict,
|
||||
resolve_ui_mime_type,
|
||||
)
|
||||
from fastmcp.server.auth import AuthContext, AuthProvider, run_auth_checks
|
||||
from fastmcp.server.auth import AuthCheck, AuthContext, AuthProvider, run_auth_checks
|
||||
from fastmcp.server.dependencies import get_access_token
|
||||
from fastmcp.server.lifespan import Lifespan
|
||||
from fastmcp.server.low_level import LowLevelServer
|
||||
|
|
@ -78,7 +78,7 @@ from fastmcp.server.transforms import (
|
|||
from fastmcp.server.transforms.visibility import apply_session_transforms, is_enabled
|
||||
from fastmcp.settings import DuplicateBehavior as DuplicateBehaviorSetting
|
||||
from fastmcp.tools.function_tool import FunctionTool
|
||||
from fastmcp.tools.tool import AuthCheckCallable, Tool, ToolResult
|
||||
from fastmcp.tools.tool import Tool, ToolResult
|
||||
from fastmcp.tools.tool_transform import ToolTransformConfig
|
||||
from fastmcp.utilities.components import FastMCPComponent
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
|
@ -509,7 +509,7 @@ class FastMCP(
|
|||
if not skip_auth and tool.auth is not None:
|
||||
ctx = AuthContext(token=token, component=tool)
|
||||
try:
|
||||
if not run_auth_checks(tool.auth, ctx):
|
||||
if not await run_auth_checks(tool.auth, ctx):
|
||||
continue
|
||||
except AuthorizationError:
|
||||
continue
|
||||
|
|
@ -540,7 +540,7 @@ class FastMCP(
|
|||
if not skip_auth and tool.auth is not None:
|
||||
ctx = AuthContext(token=token, component=tool)
|
||||
try:
|
||||
if not run_auth_checks(tool.auth, ctx):
|
||||
if not await run_auth_checks(tool.auth, ctx):
|
||||
return None
|
||||
except AuthorizationError:
|
||||
return None
|
||||
|
|
@ -607,7 +607,7 @@ class FastMCP(
|
|||
if not skip_auth and resource.auth is not None:
|
||||
ctx = AuthContext(token=token, component=resource)
|
||||
try:
|
||||
if not run_auth_checks(resource.auth, ctx):
|
||||
if not await run_auth_checks(resource.auth, ctx):
|
||||
continue
|
||||
except AuthorizationError:
|
||||
continue
|
||||
|
|
@ -638,7 +638,7 @@ class FastMCP(
|
|||
if not skip_auth and resource.auth is not None:
|
||||
ctx = AuthContext(token=token, component=resource)
|
||||
try:
|
||||
if not run_auth_checks(resource.auth, ctx):
|
||||
if not await run_auth_checks(resource.auth, ctx):
|
||||
return None
|
||||
except AuthorizationError:
|
||||
return None
|
||||
|
|
@ -706,7 +706,7 @@ class FastMCP(
|
|||
if not skip_auth and template.auth is not None:
|
||||
ctx = AuthContext(token=token, component=template)
|
||||
try:
|
||||
if not run_auth_checks(template.auth, ctx):
|
||||
if not await run_auth_checks(template.auth, ctx):
|
||||
continue
|
||||
except AuthorizationError:
|
||||
continue
|
||||
|
|
@ -737,7 +737,7 @@ class FastMCP(
|
|||
if not skip_auth and template.auth is not None:
|
||||
ctx = AuthContext(token=token, component=template)
|
||||
try:
|
||||
if not run_auth_checks(template.auth, ctx):
|
||||
if not await run_auth_checks(template.auth, ctx):
|
||||
return None
|
||||
except AuthorizationError:
|
||||
return None
|
||||
|
|
@ -801,7 +801,7 @@ class FastMCP(
|
|||
if not skip_auth and prompt.auth is not None:
|
||||
ctx = AuthContext(token=token, component=prompt)
|
||||
try:
|
||||
if not run_auth_checks(prompt.auth, ctx):
|
||||
if not await run_auth_checks(prompt.auth, ctx):
|
||||
continue
|
||||
except AuthorizationError:
|
||||
continue
|
||||
|
|
@ -832,7 +832,7 @@ class FastMCP(
|
|||
if not skip_auth and prompt.auth is not None:
|
||||
ctx = AuthContext(token=token, component=prompt)
|
||||
try:
|
||||
if not run_auth_checks(prompt.auth, ctx):
|
||||
if not await run_auth_checks(prompt.auth, ctx):
|
||||
return None
|
||||
except AuthorizationError:
|
||||
return None
|
||||
|
|
@ -1283,7 +1283,7 @@ class FastMCP(
|
|||
app: AppConfig | dict[str, Any] | bool | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionTool: ...
|
||||
|
||||
@overload
|
||||
|
|
@ -1304,7 +1304,7 @@ class FastMCP(
|
|||
app: AppConfig | dict[str, Any] | bool | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[AnyFunction], FunctionTool]: ...
|
||||
|
||||
def tool(
|
||||
|
|
@ -1324,7 +1324,7 @@ class FastMCP(
|
|||
app: AppConfig | dict[str, Any] | bool | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> (
|
||||
Callable[[AnyFunction], FunctionTool]
|
||||
| FunctionTool
|
||||
|
|
@ -1445,7 +1445,7 @@ class FastMCP(
|
|||
meta: dict[str, Any] | None = None,
|
||||
app: AppConfig | dict[str, Any] | bool | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction]:
|
||||
"""Decorator to register a function as a resource.
|
||||
|
||||
|
|
@ -1576,7 +1576,7 @@ class FastMCP(
|
|||
tags: set[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionPrompt: ...
|
||||
|
||||
@overload
|
||||
|
|
@ -1592,7 +1592,7 @@ class FastMCP(
|
|||
tags: set[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[AnyFunction], FunctionPrompt]: ...
|
||||
|
||||
def prompt(
|
||||
|
|
@ -1607,7 +1607,7 @@ class FastMCP(
|
|||
tags: set[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> (
|
||||
Callable[[AnyFunction], FunctionPrompt]
|
||||
| FunctionPrompt
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ from pydantic.json_schema import SkipJsonSchema
|
|||
|
||||
import fastmcp
|
||||
from fastmcp.decorators import resolve_task_config
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.dependencies import without_injected_parameters
|
||||
from fastmcp.server.tasks.config import TaskConfig
|
||||
from fastmcp.tools.function_parsing import ParsedFunction, _is_object_schema
|
||||
from fastmcp.tools.tool import (
|
||||
AuthCheckCallable,
|
||||
Tool,
|
||||
ToolResult,
|
||||
ToolResultSerializerType,
|
||||
|
|
@ -78,7 +78,7 @@ class ToolMeta:
|
|||
exclude_args: list[str] | None = None
|
||||
serializer: Any | None = None
|
||||
timeout: float | None = None
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None
|
||||
auth: AuthCheck | list[AuthCheck] | None = None
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ class FunctionTool(Tool):
|
|||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionTool:
|
||||
"""Create a FunctionTool from a function.
|
||||
|
||||
|
|
@ -345,7 +345,7 @@ def tool(
|
|||
exclude_args: list[str] | None = None,
|
||||
serializer: Any | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[F], F]: ...
|
||||
@overload
|
||||
def tool(
|
||||
|
|
@ -364,7 +364,7 @@ def tool(
|
|||
exclude_args: list[str] | None = None,
|
||||
serializer: Any | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Callable[[F], F]: ...
|
||||
|
||||
|
||||
|
|
@ -384,7 +384,7 @@ def tool(
|
|||
exclude_args: list[str] | None = None,
|
||||
serializer: Any | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> Any:
|
||||
"""Standalone decorator to mark a function as an MCP tool.
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from mcp.types import Tool as MCPTool
|
|||
from pydantic import BaseModel, Field, model_validator
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
|
||||
from fastmcp.utilities.components import FastMCPComponent
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
|
@ -37,10 +38,6 @@ from fastmcp.utilities.types import (
|
|||
NotSetT,
|
||||
)
|
||||
|
||||
# Runtime type alias for auth checks to avoid circular imports with authorization.py
|
||||
# AuthCheck is Callable[[AuthContext], bool] but we use Any to avoid the import
|
||||
AuthCheckCallable: TypeAlias = Callable[[Any], bool]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from docket import Docket
|
||||
from docket.execution import Execution
|
||||
|
|
@ -147,7 +144,7 @@ class Tool(FastMCPComponent):
|
|||
),
|
||||
] = None
|
||||
auth: Annotated[
|
||||
SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None],
|
||||
SkipJsonSchema[AuthCheck | list[AuthCheck] | None],
|
||||
Field(description="Authorization checks for this tool", exclude=True),
|
||||
] = None
|
||||
timeout: Annotated[
|
||||
|
|
@ -207,7 +204,7 @@ class Tool(FastMCPComponent):
|
|||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
) -> FunctionTool:
|
||||
"""Create a Tool from a function."""
|
||||
from fastmcp.tools.function_tool import FunctionTool
|
||||
|
|
|
|||
|
|
@ -120,31 +120,31 @@ class TestRestrictTag:
|
|||
|
||||
|
||||
class TestRunAuthChecks:
|
||||
def test_single_check_passes(self):
|
||||
async def test_single_check_passes(self):
|
||||
ctx = AuthContext(token=make_token(scopes=["test"]), component=make_tool())
|
||||
assert run_auth_checks(require_scopes("test"), ctx) is True
|
||||
assert await run_auth_checks(require_scopes("test"), ctx) is True
|
||||
|
||||
def test_single_check_fails(self):
|
||||
async def test_single_check_fails(self):
|
||||
ctx = AuthContext(token=None, component=make_tool())
|
||||
assert run_auth_checks(require_scopes("test"), ctx) is False
|
||||
assert await run_auth_checks(require_scopes("test"), ctx) is False
|
||||
|
||||
def test_multiple_checks_all_pass(self):
|
||||
async def test_multiple_checks_all_pass(self):
|
||||
token = make_token(scopes=["test", "admin"])
|
||||
ctx = AuthContext(token=token, component=make_tool())
|
||||
checks = [require_scopes("test"), require_scopes("admin")]
|
||||
assert run_auth_checks(checks, ctx) is True
|
||||
assert await run_auth_checks(checks, ctx) is True
|
||||
|
||||
def test_multiple_checks_one_fails(self):
|
||||
async def test_multiple_checks_one_fails(self):
|
||||
token = make_token(scopes=["read"])
|
||||
ctx = AuthContext(token=token, component=make_tool())
|
||||
checks = [require_scopes("read"), require_scopes("admin")]
|
||||
assert run_auth_checks(checks, ctx) is False
|
||||
assert await run_auth_checks(checks, ctx) is False
|
||||
|
||||
def test_empty_list_passes(self):
|
||||
async def test_empty_list_passes(self):
|
||||
ctx = AuthContext(token=None, component=make_tool())
|
||||
assert run_auth_checks([], ctx) is True
|
||||
assert await run_auth_checks([], ctx) is True
|
||||
|
||||
def test_custom_lambda_check(self):
|
||||
async def test_custom_lambda_check(self):
|
||||
token = make_token()
|
||||
token.claims = {"level": 5}
|
||||
ctx = AuthContext(token=token, component=make_tool())
|
||||
|
|
@ -152,9 +152,9 @@ class TestRunAuthChecks:
|
|||
def check(ctx: AuthContext) -> bool:
|
||||
return ctx.token is not None and ctx.token.claims.get("level", 0) >= 3
|
||||
|
||||
assert run_auth_checks(check, ctx) is True
|
||||
assert await run_auth_checks(check, ctx) is True
|
||||
|
||||
def test_authorization_error_propagates(self):
|
||||
async def test_authorization_error_propagates(self):
|
||||
"""AuthorizationError from auth check should propagate with custom message."""
|
||||
from fastmcp.exceptions import AuthorizationError
|
||||
|
||||
|
|
@ -163,9 +163,9 @@ class TestRunAuthChecks:
|
|||
|
||||
ctx = AuthContext(token=make_token(), component=make_tool())
|
||||
with pytest.raises(AuthorizationError, match="Custom denial reason"):
|
||||
run_auth_checks(custom_auth_check, ctx)
|
||||
await run_auth_checks(custom_auth_check, ctx)
|
||||
|
||||
def test_generic_exception_is_masked(self):
|
||||
async def test_generic_exception_is_masked(self):
|
||||
"""Generic exceptions from auth checks should be masked (return False)."""
|
||||
|
||||
def buggy_auth_check(ctx: AuthContext) -> bool:
|
||||
|
|
@ -173,9 +173,9 @@ class TestRunAuthChecks:
|
|||
|
||||
ctx = AuthContext(token=make_token(), component=make_tool())
|
||||
# Should return False, not raise the ValueError
|
||||
assert run_auth_checks(buggy_auth_check, ctx) is False
|
||||
assert await run_auth_checks(buggy_auth_check, ctx) is False
|
||||
|
||||
def test_authorization_error_stops_chain(self):
|
||||
async def test_authorization_error_stops_chain(self):
|
||||
"""AuthorizationError should stop the check chain and propagate."""
|
||||
from fastmcp.exceptions import AuthorizationError
|
||||
|
||||
|
|
@ -195,11 +195,62 @@ class TestRunAuthChecks:
|
|||
|
||||
ctx = AuthContext(token=make_token(), component=make_tool())
|
||||
with pytest.raises(AuthorizationError, match="Explicit denial"):
|
||||
run_auth_checks([check_1, check_2, check_3], ctx)
|
||||
await run_auth_checks([check_1, check_2, check_3], ctx)
|
||||
|
||||
# Check 3 should not be called
|
||||
assert call_order == [1, 2]
|
||||
|
||||
async def test_async_check_passes(self):
|
||||
"""Async auth check functions should be awaited."""
|
||||
|
||||
async def async_check(ctx: AuthContext) -> bool:
|
||||
return ctx.token is not None
|
||||
|
||||
ctx = AuthContext(token=make_token(), component=make_tool())
|
||||
assert await run_auth_checks(async_check, ctx) is True
|
||||
|
||||
async def test_async_check_fails(self):
|
||||
"""Async auth check that returns False should deny access."""
|
||||
|
||||
async def async_check(ctx: AuthContext) -> bool:
|
||||
return False
|
||||
|
||||
ctx = AuthContext(token=make_token(), component=make_tool())
|
||||
assert await run_auth_checks(async_check, ctx) is False
|
||||
|
||||
async def test_mixed_sync_and_async_checks(self):
|
||||
"""A mix of sync and async checks should all be evaluated."""
|
||||
|
||||
def sync_check(ctx: AuthContext) -> bool:
|
||||
return True
|
||||
|
||||
async def async_check(ctx: AuthContext) -> bool:
|
||||
return ctx.token is not None
|
||||
|
||||
ctx = AuthContext(token=make_token(scopes=["test"]), component=make_tool())
|
||||
checks = [sync_check, async_check, require_scopes("test")]
|
||||
assert await run_auth_checks(checks, ctx) is True
|
||||
|
||||
async def test_async_check_exception_is_masked(self):
|
||||
"""Async checks that raise non-AuthorizationError should be masked."""
|
||||
|
||||
async def buggy_async_check(ctx: AuthContext) -> bool:
|
||||
raise ValueError("async error")
|
||||
|
||||
ctx = AuthContext(token=make_token(), component=make_tool())
|
||||
assert await run_auth_checks(buggy_async_check, ctx) is False
|
||||
|
||||
async def test_async_check_authorization_error_propagates(self):
|
||||
"""Async checks that raise AuthorizationError should propagate."""
|
||||
from fastmcp.exceptions import AuthorizationError
|
||||
|
||||
async def async_denial(ctx: AuthContext) -> bool:
|
||||
raise AuthorizationError("Async denial")
|
||||
|
||||
ctx = AuthContext(token=make_token(), component=make_tool())
|
||||
with pytest.raises(AuthorizationError, match="Async denial"):
|
||||
await run_auth_checks(async_denial, ctx)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tests for tool-level auth with FastMCP
|
||||
|
|
@ -454,6 +505,91 @@ class TestAuthIntegration:
|
|||
auth_context_var.reset(tok)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Integration tests with async auth checks
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestAsyncAuthIntegration:
|
||||
async def test_async_auth_check_filters_tool_listing(self):
|
||||
"""Async auth checks should work for filtering tool lists."""
|
||||
mcp = FastMCP()
|
||||
|
||||
async def check_claims(ctx: AuthContext) -> bool:
|
||||
return ctx.token is not None and ctx.token.claims.get("role") == "admin"
|
||||
|
||||
@mcp.tool(auth=check_claims)
|
||||
def admin_tool() -> str:
|
||||
return "admin"
|
||||
|
||||
@mcp.tool
|
||||
def public_tool() -> str:
|
||||
return "public"
|
||||
|
||||
# Without token, only public tool visible
|
||||
tools = await mcp.list_tools()
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "public_tool"
|
||||
|
||||
# With correct claims, both visible
|
||||
token = make_token()
|
||||
token.claims = {"role": "admin"}
|
||||
tok = set_token(token)
|
||||
try:
|
||||
tools = await mcp.list_tools()
|
||||
assert len(tools) == 2
|
||||
finally:
|
||||
auth_context_var.reset(tok)
|
||||
|
||||
async def test_async_auth_check_on_tool_call(self):
|
||||
"""Async auth checks should work for tool execution via client."""
|
||||
mcp = FastMCP()
|
||||
|
||||
async def check_claims(ctx: AuthContext) -> bool:
|
||||
return ctx.token is not None and ctx.token.claims.get("role") == "admin"
|
||||
|
||||
@mcp.tool(auth=check_claims)
|
||||
def admin_tool() -> str:
|
||||
return "secret"
|
||||
|
||||
token = make_token()
|
||||
token.claims = {"role": "admin"}
|
||||
tok = set_token(token)
|
||||
try:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("admin_tool", {})
|
||||
assert result.content[0].text == "secret"
|
||||
finally:
|
||||
auth_context_var.reset(tok)
|
||||
|
||||
async def test_async_auth_middleware(self):
|
||||
"""Async auth checks should work with AuthMiddleware."""
|
||||
|
||||
async def async_scope_check(ctx: AuthContext) -> bool:
|
||||
return ctx.token is not None and "api" in ctx.token.scopes
|
||||
|
||||
mcp = FastMCP(middleware=[AuthMiddleware(auth=async_scope_check)])
|
||||
|
||||
@mcp.tool
|
||||
def api_tool() -> str:
|
||||
return "api"
|
||||
|
||||
# Without token, tool is hidden
|
||||
result = await mcp._list_tools_mcp(__import__("mcp").types.ListToolsRequest())
|
||||
assert len(result.tools) == 0
|
||||
|
||||
# With token containing "api" scope, tool is visible
|
||||
token = make_token(scopes=["api"])
|
||||
tok = set_token(token)
|
||||
try:
|
||||
result = await mcp._list_tools_mcp(
|
||||
__import__("mcp").types.ListToolsRequest()
|
||||
)
|
||||
assert len(result.tools) == 1
|
||||
finally:
|
||||
auth_context_var.reset(tok)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tests for transformed tools preserving auth
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue