diff --git a/AGENTS.md b/AGENTS.md
index 1d6ff4e9b..c726c0504 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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
diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx
index 93420c6b8..f91734ba6 100644
--- a/docs/development/v3-notes/v3-features.mdx
+++ b/docs/development/v3-notes/v3-features.mdx
@@ -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.
diff --git a/docs/python-sdk/fastmcp-resources-template.mdx b/docs/python-sdk/fastmcp-resources-template.mdx
index e55bae102..0baaedbd6 100644
--- a/docs/python-sdk/fastmcp-resources-template.mdx
+++ b/docs/python-sdk/fastmcp-resources-template.mdx
@@ -62,7 +62,7 @@ A template for dynamically creating resources.
#### `from_function`
```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`
@@ -237,7 +237,7 @@ FunctionResourceTemplate splats the params dict since .fn expects **kwargs.
#### `from_function`
```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.
diff --git a/docs/python-sdk/fastmcp-server-auth-authorization.mdx b/docs/python-sdk/fastmcp-server-auth-authorization.mdx
index 6268118d8..28663f6f8 100644
--- a/docs/python-sdk/fastmcp-server-auth-authorization.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-authorization.mdx
@@ -36,7 +36,7 @@ Example:
## Functions
-### `require_scopes`
+### `require_scopes`
```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`
+### `restrict_tag`
```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`
+### `run_auth_checks`
```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`
+### `AuthContext`
Context passed to auth check callables.
@@ -115,7 +117,7 @@ access to the current authentication token and the component being accessed.
**Methods:**
-#### `tool`
+#### `tool`
```python
tool(self) -> Tool | None
diff --git a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx b/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx
index bfb3ccea1..44558f560 100644
--- a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx
@@ -14,7 +14,7 @@ registration functionality to LocalProvider.
## Classes
-### `ToolDecoratorMixin`
+### `ToolDecoratorMixin`
Mixin class providing tool decorator functionality for LocalProvider.
@@ -26,7 +26,7 @@ This mixin contains all methods related to:
**Methods:**
-#### `add_tool`
+#### `add_tool`
```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`
+#### `tool`
```python
tool(self: LocalProvider, name_or_fn: AnyFunction) -> FunctionTool
```
-#### `tool`
+#### `tool`
```python
tool(self: LocalProvider, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool]
```
-#### `tool`
+#### `tool`
```python
tool(self: LocalProvider, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
diff --git a/docs/python-sdk/fastmcp-tools-tool.mdx b/docs/python-sdk/fastmcp-tools-tool.mdx
index 1068a7716..f13d63f1c 100644
--- a/docs/python-sdk/fastmcp-tools-tool.mdx
+++ b/docs/python-sdk/fastmcp-tools-tool.mdx
@@ -7,7 +7,7 @@ sidebarTitle: tool
## Functions
-### `default_serializer`
+### `default_serializer`
```python
default_serializer(data: Any) -> str
@@ -15,17 +15,17 @@ default_serializer(data: Any) -> str
## Classes
-### `ToolResult`
+### `ToolResult`
**Methods:**
-#### `to_mcp_result`
+#### `to_mcp_result`
```python
to_mcp_result(self) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult
```
-### `Tool`
+### `Tool`
Internal tool registration info.
@@ -33,7 +33,7 @@ Internal tool registration info.
**Methods:**
-#### `to_mcp_tool`
+#### `to_mcp_tool`
```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`
+#### `from_function`
```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`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -66,7 +66,7 @@ implemented by subclasses.
(list of ContentBlocks, dict of structured output).
-#### `convert_result`
+#### `convert_result`
```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`
+#### `register_with_docket`
```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`
+#### `add_to_docket`
```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`
+#### `from_tool`
```python
from_tool(cls, tool: Tool) -> TransformedTool
```
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
diff --git a/docs/servers/authorization.mdx b/docs/servers/authorization.mdx
index 0ad0d569e..a48d2a9e8 100644
--- a/docs/servers/authorization.mdx
+++ b/docs/servers/authorization.mdx
@@ -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
diff --git a/src/fastmcp/prompts/function_prompt.py b/src/fastmcp/prompts/function_prompt.py
index 916838ede..a58700a01 100644
--- a/src/fastmcp/prompts/function_prompt.py
+++ b/src/fastmcp/prompts/function_prompt.py
@@ -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.
diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py
index 8cf2d3265..07540629e 100644
--- a/src/fastmcp/prompts/prompt.py
+++ b/src/fastmcp/prompts/prompt.py
@@ -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.
diff --git a/src/fastmcp/resources/function_resource.py b/src/fastmcp/resources/function_resource.py
index 76c6f974c..bf6673552 100644
--- a/src/fastmcp/resources/function_resource.py
+++ b/src/fastmcp/resources/function_resource.py
@@ -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.
diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py
index 1f36fd5e2..26ed535de 100644
--- a/src/fastmcp/resources/resource.py
+++ b/src/fastmcp/resources/resource.py
@@ -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,
diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py
index 8ce650073..c2fb1b622 100644
--- a/src/fastmcp/resources/template.py
+++ b/src/fastmcp/resources/template.py
@@ -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."""
diff --git a/src/fastmcp/server/auth/authorization.py b/src/fastmcp/server/auth/authorization.py
index dd0e16cc1..8455b81f5 100644
--- a/src/fastmcp/server/auth/authorization.py
+++ b/src/fastmcp/server/auth/authorization.py
@@ -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
diff --git a/src/fastmcp/server/middleware/authorization.py b/src/fastmcp/server/middleware/authorization.py
index 6a50ed656..abe33447b 100644
--- a/src/fastmcp/server/middleware/authorization.py
+++ b/src/fastmcp/server/middleware/authorization.py
@@ -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"
)
diff --git a/src/fastmcp/server/providers/local_provider/decorators/prompts.py b/src/fastmcp/server/providers/local_provider/decorators/prompts.py
index a25d8fa52..5e01b7a04 100644
--- a/src/fastmcp/server/providers/local_provider/decorators/prompts.py
+++ b/src/fastmcp/server/providers/local_provider/decorators/prompts.py
@@ -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
diff --git a/src/fastmcp/server/providers/local_provider/decorators/resources.py b/src/fastmcp/server/providers/local_provider/decorators/resources.py
index f6985b164..52314378e 100644
--- a/src/fastmcp/server/providers/local_provider/decorators/resources.py
+++ b/src/fastmcp/server/providers/local_provider/decorators/resources.py
@@ -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.
diff --git a/src/fastmcp/server/providers/local_provider/decorators/tools.py b/src/fastmcp/server/providers/local_provider/decorators/tools.py
index 7527f1b76..c59fa8a3f 100644
--- a/src/fastmcp/server/providers/local_provider/decorators/tools.py
+++ b/src/fastmcp/server/providers/local_provider/decorators/tools.py
@@ -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
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 66c60d978..bd0e5e264 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -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
diff --git a/src/fastmcp/tools/function_tool.py b/src/fastmcp/tools/function_tool.py
index 812f5108c..6c1a361f6 100644
--- a/src/fastmcp/tools/function_tool.py
+++ b/src/fastmcp/tools/function_tool.py
@@ -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.
diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py
index e13cda280..bdacdac58 100644
--- a/src/fastmcp/tools/tool.py
+++ b/src/fastmcp/tools/tool.py
@@ -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
diff --git a/tests/server/auth/test_authorization.py b/tests/server/auth/test_authorization.py
index 6bab4cecd..4bd0dff9a 100644
--- a/tests/server/auth/test_authorization.py
+++ b/tests/server/auth/test_authorization.py
@@ -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
# =============================================================================