Decouple component imports from server (#4150)

This commit is contained in:
Jeremiah Lowin 2026-05-15 11:49:08 -04:00 committed by GitHub
commit d8dcc273ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 325 additions and 375 deletions

View file

@ -136,6 +136,7 @@ gh api -X POST repos/PrefectHQ/fastmcp/releases/generate-notes \
### Module Exports
- **Do not create overeager `__init__.py` files.** Package initializers should not import heavy submodules, provider stacks, optional integrations, or modules that can point back into the package. Overeager re-exports make the framework sprawl and create circular imports that only appear in fresh interpreters or clean installs.
- **Be intentional about re-exports** - don't blindly re-export everything to parent namespaces
- Core types that define a module's purpose should be exported (e.g., `Middleware` from `fastmcp.server.middleware`)
- Specialized features can live in submodules (e.g., `fastmcp.server.middleware.dynamic`)

View file

@ -7,7 +7,8 @@ This package contains the app-related components:
- ``ResourceCSP`` / ``ResourcePermissions`` security configuration
"""
from fastmcp.apps.app import FastMCPApp as FastMCPApp
from typing import TYPE_CHECKING as _TYPE_CHECKING
from fastmcp.apps.config import AppConfig as AppConfig
from fastmcp.apps.config import PrefabAppConfig as PrefabAppConfig
from fastmcp.apps.config import ResourceCSP as ResourceCSP
@ -16,3 +17,26 @@ from fastmcp.apps.config import UI_EXTENSION_ID as UI_EXTENSION_ID
from fastmcp.apps.config import app_config_to_meta_dict as app_config_to_meta_dict
from fastmcp.utilities.mime import UI_MIME_TYPE as UI_MIME_TYPE
from fastmcp.utilities.mime import resolve_ui_mime_type as resolve_ui_mime_type
__all__ = [
"UI_EXTENSION_ID",
"UI_MIME_TYPE",
"AppConfig",
"FastMCPApp",
"PrefabAppConfig",
"ResourceCSP",
"ResourcePermissions",
"app_config_to_meta_dict",
"resolve_ui_mime_type",
]
if _TYPE_CHECKING:
from fastmcp.apps.app import FastMCPApp as FastMCPApp
def __getattr__(name: str) -> object:
if name == "FastMCPApp":
from fastmcp.apps.app import FastMCPApp
return FastMCPApp
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View file

@ -30,16 +30,18 @@ from __future__ import annotations
import inspect
from collections.abc import AsyncIterator, Callable, Sequence
from contextlib import asynccontextmanager
from typing import Any, Literal, TypeVar, overload
from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
from mcp.types import AnyFunction, Icon, ToolAnnotations
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.providers.base import Provider
from fastmcp.server.providers.local_provider import LocalProvider
from fastmcp.tools.base import Tool
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
from fastmcp.server.providers.local_provider import LocalProvider
from fastmcp.tools.base import Tool
logger = get_logger(__name__)
F = TypeVar("F", bound=Callable[..., Any])
@ -149,9 +151,11 @@ class FastMCPApp(Provider):
"""
def __init__(self, name: str) -> None:
from fastmcp.server.providers.local_provider import LocalProvider
super().__init__()
self.name = name
self._local = LocalProvider(on_duplicate="error")
self._local: LocalProvider = LocalProvider(on_duplicate="error")
def __repr__(self) -> str:
return f"FastMCPApp({self.name!r})"
@ -215,6 +219,8 @@ class FastMCPApp(Provider):
)
def _register(fn: F, tool_name: str | None) -> F:
from fastmcp.tools.base import Tool
resolved_name = tool_name or getattr(fn, "__name__", None)
if resolved_name is None:
raise ValueError(f"Cannot determine tool name for {fn!r}")
@ -315,6 +321,7 @@ class FastMCPApp(Provider):
from fastmcp.server.providers.local_provider.decorators.tools import (
PREFAB_RENDERER_URI,
)
from fastmcp.tools.base import Tool
resolved = tool_name or getattr(fn, "__name__", None) or "unknown"
app_config = AppConfig(
@ -360,6 +367,8 @@ class FastMCPApp(Provider):
The tool is tagged with this app's name for routing.
"""
from fastmcp.tools.base import Tool
if not isinstance(tool, Tool):
tool = Tool._ensure_tool(tool)

View file

@ -30,10 +30,10 @@ from pydantic import Field
from pydantic.json_schema import SkipJsonSchema
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.tasks import TaskConfig, TaskMeta
from fastmcp.utilities.types import (
FastMCPBaseModel,
)

View file

@ -26,19 +26,15 @@ import fastmcp
from fastmcp.decorators import resolve_task_config
from fastmcp.exceptions import FastMCPDeprecationWarning, FastMCPError, PromptError
from fastmcp.prompts.base 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.utilities.async_utils import (
call_sync_fn_in_threadpool,
is_coroutine_function,
)
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.docstring_parsing import ParsedDocstring, parse_docstring
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.tasks import TaskConfig
from fastmcp.utilities.types import get_cached_typeadapter
if TYPE_CHECKING:
@ -193,6 +189,11 @@ class FunctionPrompt(Prompt):
)
# Transform Context type annotations to Depends() for unified DI
from fastmcp.server.dependencies import (
transform_context_annotations,
without_injected_parameters,
)
fn = transform_context_annotations(fn)
# Wrap fn to handle dependency resolution internally

View file

@ -31,9 +31,9 @@ from pydantic.json_schema import SkipJsonSchema
from typing_extensions import Self
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.tasks import TaskConfig, TaskMeta
class ResourceContent(pydantic.BaseModel):

View file

@ -17,17 +17,13 @@ import fastmcp
from fastmcp.decorators import resolve_task_config
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.resources.base import Resource, ResourceResult
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.utilities.async_utils import (
call_sync_fn_in_threadpool,
is_coroutine_function,
)
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.mime import resolve_ui_mime_type
from fastmcp.utilities.tasks import TaskConfig
if TYPE_CHECKING:
from docket import Docket
@ -182,6 +178,11 @@ class FunctionResource(Resource):
fn = fn.__func__
# Transform Context type annotations to Depends() for unified DI
from fastmcp.server.dependencies import (
transform_context_annotations,
without_injected_parameters,
)
fn = transform_context_annotations(fn)
# Wrap fn to handle dependency resolution internally

View file

@ -24,15 +24,11 @@ from pydantic import (
)
from fastmcp.resources.base import Resource, ResourceResult
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.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.mime import resolve_ui_mime_type
from fastmcp.utilities.tasks import TaskConfig, TaskMeta
from fastmcp.utilities.types import get_cached_typeadapter
@ -580,6 +576,11 @@ class FunctionResourceTemplate(ResourceTemplate):
raise ValueError("URI template must contain at least one parameter")
# Use wrapper to get user-facing parameters (excludes injected params)
from fastmcp.server.dependencies import (
transform_context_annotations,
without_injected_parameters,
)
wrapper_fn = without_injected_parameters(fn)
user_sig = inspect.signature(wrapper_fn)
func_params = set(user_sig.parameters.keys())

View file

@ -1,182 +1,17 @@
"""Authorization checks for FastMCP components.
"""Backward-compatible exports for component authorization primitives."""
This module provides callable-based authorization for tools, resources, and prompts.
Auth checks are functions that receive an AuthContext and return True to allow access
or False to deny.
from fastmcp.utilities.authorization import (
AuthCheck,
AuthContext,
require_scopes,
restrict_tag,
run_auth_checks,
)
Auth checks can also raise exceptions:
- AuthorizationError: Propagates with the custom message for explicit denial
- Other exceptions: Masked for security (logged, treated as auth failure)
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.auth import require_scopes
mcp = FastMCP()
@mcp.tool(auth=require_scopes("write"))
def protected_tool(): ...
@mcp.resource("data://secret", auth=require_scopes("read"))
def secret_data(): ...
@mcp.prompt(auth=require_scopes("admin"))
def admin_prompt(): ...
```
"""
from __future__ import annotations
import inspect
import logging
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
from fastmcp.exceptions import AuthorizationError
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from fastmcp.server.auth import AccessToken
from fastmcp.tools.base import Tool
from fastmcp.utilities.components import FastMCPComponent
@dataclass
class AuthContext:
"""Context passed to auth check callables.
This object is passed to each auth check function and provides
access to the current authentication token and the component being accessed.
Attributes:
token: The current access token, or None if unauthenticated.
component: The component (tool, resource, or prompt) being accessed.
tool: Backwards-compatible alias for component when it's a Tool.
"""
token: AccessToken | None
component: FastMCPComponent
@property
def tool(self) -> Tool | None:
"""Backwards-compatible access to the component as a Tool.
Returns the component if it's a Tool, None otherwise.
"""
from fastmcp.tools.base import Tool
return self.component if isinstance(self.component, Tool) else None
# Type alias for auth check functions (sync or async)
AuthCheck = Callable[[AuthContext], bool] | Callable[[AuthContext], Awaitable[bool]]
def require_scopes(*scopes: str) -> AuthCheck:
"""Require specific OAuth scopes.
Returns an auth check that requires ALL specified scopes to be present
in the token (AND logic).
Args:
*scopes: One or more scope strings that must all be present.
Example:
```python
@mcp.tool(auth=require_scopes("admin"))
def admin_tool(): ...
@mcp.tool(auth=require_scopes("read", "write"))
def read_write_tool(): ...
```
"""
required = set(scopes)
def check(ctx: AuthContext) -> bool:
if ctx.token is None:
return False
return required.issubset(set(ctx.token.scopes))
return check
def restrict_tag(tag: str, *, scopes: list[str]) -> AuthCheck:
"""Restrict components with a specific tag to require certain scopes.
If the component has the specified tag, the token must have ALL the
required scopes. If the component doesn't have the tag, access is allowed.
Args:
tag: The tag that triggers the scope requirement.
scopes: List of scopes required when the tag is present.
Example:
```python
# Components tagged "admin" require the "admin" scope
AuthMiddleware(auth=restrict_tag("admin", scopes=["admin"]))
```
"""
required = set(scopes)
def check(ctx: AuthContext) -> bool:
if tag not in ctx.component.tags:
return True # Tag not present, no restriction
if ctx.token is None:
return False
return required.issubset(set(ctx.token.scopes))
return check
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. Checks can be
synchronous or asynchronous functions.
Auth checks can:
- Return True to allow access
- Return False to deny access
- Raise AuthorizationError to deny with a custom message (propagates)
- Raise other exceptions (masked for security, treated as denial)
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:
True if all checks pass, False if any check fails.
Raises:
AuthorizationError: If an auth check explicitly raises it.
"""
check_list = [checks] if not isinstance(checks, list) else checks
check_list = cast(list[AuthCheck], check_list)
for check in check_list:
try:
result = check(ctx)
if inspect.isawaitable(result):
result = await result
if not result:
return False
except AuthorizationError:
# Let AuthorizationError propagate with its custom message
raise
except Exception:
# Mask other exceptions for security - log and treat as auth failure
logger.warning(
f"Auth check {getattr(check, '__name__', repr(check))} "
"raised an unexpected exception",
exc_info=True,
)
return False
return True
__all__ = [
"AuthCheck",
"AuthContext",
"require_scopes",
"restrict_tag",
"run_auth_checks",
]

View file

@ -35,17 +35,17 @@ from typing import TYPE_CHECKING, Literal, cast
from typing_extensions import Self
from fastmcp.prompts.base import Prompt
from fastmcp.resources.base import Resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.transforms.visibility import Visibility
from fastmcp.tools.base import Tool
from fastmcp.utilities.async_utils import gather
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.versions import VersionSpec, version_sort_key
if TYPE_CHECKING:
from fastmcp.prompts.base import Prompt
from fastmcp.resources.base import Resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.transforms import Transform
from fastmcp.tools.base import Tool
class Provider:
@ -480,10 +480,10 @@ class Provider:
self._list_resource_templates(),
self._list_prompts(),
)
tools = cast(Sequence[Tool], results[0])
resources = cast(Sequence[Resource], results[1])
templates = cast(Sequence[ResourceTemplate], results[2])
prompts = cast(Sequence[Prompt], results[3])
tools = cast("Sequence[Tool]", results[0])
resources = cast("Sequence[Resource]", results[1])
templates = cast("Sequence[ResourceTemplate]", results[2])
prompts = cast("Sequence[Prompt]", results[3])
# Apply provider's own transforms sequentially
# For tasks, we need the fully-transformed names

View file

@ -1,147 +1,19 @@
"""TaskConfig for MCP SEP-1686 background task execution modes.
"""Backward-compatible exports for task configuration primitives."""
This module defines the configuration for how tools, resources, and prompts
handle task-augmented execution as specified in SEP-1686.
"""
from fastmcp.utilities.tasks import (
DEFAULT_POLL_INTERVAL,
DEFAULT_POLL_INTERVAL_MS,
DEFAULT_TTL_MS,
TaskConfig,
TaskMeta,
TaskMode,
)
from __future__ import annotations
import functools
import inspect
from collections.abc import Callable
from dataclasses import dataclass
from datetime import timedelta
from typing import Any, Literal
from fastmcp.utilities.async_utils import is_coroutine_function
# Task execution modes per SEP-1686 / MCP ToolExecution.taskSupport
TaskMode = Literal["forbidden", "optional", "required"]
# Default values for task metadata (single source of truth)
DEFAULT_POLL_INTERVAL = timedelta(seconds=5) # Default poll interval
DEFAULT_POLL_INTERVAL_MS = int(DEFAULT_POLL_INTERVAL.total_seconds() * 1000)
DEFAULT_TTL_MS = 60_000 # Default TTL in milliseconds
@dataclass
class TaskMeta:
"""Metadata for task-augmented execution requests.
When passed to call_tool/read_resource/get_prompt, signals that
the operation should be submitted as a background task.
Attributes:
ttl: Client-requested TTL in milliseconds. If None, uses server default.
fn_key: Docket routing key. Auto-derived from component name if None.
"""
ttl: int | None = None
fn_key: str | None = None
@dataclass
class TaskConfig:
"""Configuration for MCP background task execution (SEP-1686).
Controls how a component handles task-augmented requests:
- "forbidden": Component does not support task execution. Clients must not
request task augmentation; server returns -32601 if they do.
- "optional": Component supports both synchronous and task execution.
Client may request task augmentation or call normally.
- "required": Component requires task execution. Clients must request task
augmentation; server returns -32601 if they don't.
Important:
Task-enabled components must be available at server startup to be
registered with all Docket workers. Components added dynamically after
startup will not be registered for background execution.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.tasks import TaskConfig
mcp = FastMCP("MyServer")
# Background execution required
@mcp.tool(task=TaskConfig(mode="required"))
async def long_running_task(): ...
# Supports both modes (default when task=True)
@mcp.tool(task=TaskConfig(mode="optional"))
async def flexible_task(): ...
```
"""
mode: TaskMode = "optional"
poll_interval: timedelta = DEFAULT_POLL_INTERVAL
@classmethod
def from_bool(cls, value: bool) -> TaskConfig:
"""Convert boolean task flag to TaskConfig.
Args:
value: True for "optional" mode, False for "forbidden" mode.
Returns:
TaskConfig with appropriate mode.
"""
return cls(mode="optional" if value else "forbidden")
def supports_tasks(self) -> bool:
"""Check if this component supports task execution.
Returns:
True if mode is "optional" or "required", False if "forbidden".
"""
return self.mode != "forbidden"
def validate_function(self, fn: Callable[..., Any], name: str) -> None:
"""Validate that function is compatible with this task config.
Task execution requires:
1. fastmcp[tasks] to be installed (pydocket)
2. Async functions
Raises ImportError if mode is "optional" or "required" but pydocket
is not installed. Raises ValueError if function is synchronous.
Args:
fn: The function to validate (handles callable classes and staticmethods).
name: Name for error messages.
Raises:
ImportError: If task execution is enabled but pydocket not installed.
ValueError: If task execution is enabled but function is sync.
"""
if not self.supports_tasks():
return
# Check that docket is available for task execution
# Lazy import to avoid circular: dependencies.py → http.py → tasks/__init__.py → config.py
from fastmcp.server.dependencies import require_docket
require_docket(f"`task=True` on function '{name}'")
# Unwrap callable classes and staticmethods
fn_to_check = fn
if (
not inspect.isroutine(fn)
and not isinstance(fn, functools.partial)
and callable(fn)
):
fn_to_check = fn.__call__
if isinstance(fn_to_check, staticmethod):
fn_to_check = fn_to_check.__func__
if not is_coroutine_function(fn_to_check):
raise ValueError(
f"'{name}' uses a sync function but has task execution enabled. "
"Background tasks require async functions."
)
# Note: Context IS now available in background task workers (SEP-1686)
# The wiring in _CurrentContext creates a task-aware Context with task_id
# and session from the registry. No warning needed.
__all__ = [
"DEFAULT_POLL_INTERVAL",
"DEFAULT_POLL_INTERVAL_MS",
"DEFAULT_TTL_MS",
"TaskConfig",
"TaskMeta",
"TaskMode",
]

View file

@ -27,10 +27,10 @@ from pydantic import BaseModel, Field, model_validator
from pydantic.json_schema import SkipJsonSchema
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.tasks import TaskConfig, TaskMeta
from fastmcp.utilities.types import (
Audio,
File,

View file

@ -13,10 +13,6 @@ import mcp.types
from pydantic import PydanticSchemaGenerationError
from typing_extensions import TypeVar as TypeVarExt
from fastmcp.server.dependencies import (
transform_context_annotations,
without_injected_parameters,
)
from fastmcp.tools.base import ToolResult
from fastmcp.utilities.docstring_parsing import ParsedDocstring, parse_docstring
from fastmcp.utilities.json_schema import compress_schema
@ -188,6 +184,11 @@ class ParsedFunction:
)
# Transform Context type annotations to Depends() for unified DI
from fastmcp.server.dependencies import (
transform_context_annotations,
without_injected_parameters,
)
fn = transform_context_annotations(fn)
# Handle injected parameters (Context, Docket dependencies)

View file

@ -26,9 +26,6 @@ from pydantic.json_schema import SkipJsonSchema
import fastmcp
from fastmcp.decorators import get_fastmcp_meta, resolve_task_config
from fastmcp.exceptions import FastMCPDeprecationWarning
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.base import (
Tool,
ToolResult,
@ -39,7 +36,9 @@ from fastmcp.utilities.async_utils import (
call_sync_fn_in_threadpool,
is_coroutine_function,
)
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.tasks import TaskConfig
from fastmcp.utilities.types import (
NotSet,
NotSetT,
@ -284,6 +283,8 @@ class FunctionTool(Tool):
async def run(self, arguments: dict[str, Any]) -> ToolResult:
"""Run the tool with arguments."""
from fastmcp.server.dependencies import without_injected_parameters
wrapper_fn = without_injected_parameters(
self.fn, run_in_thread=self.run_in_thread
)

View file

@ -0,0 +1,101 @@
"""Authorization checks for FastMCP components.
Auth checks are callables that receive an ``AuthContext`` and return True to
allow access or False to deny it. They can also raise ``AuthorizationError`` to
deny with a custom message; other exceptions are masked and treated as denial.
"""
from __future__ import annotations
import inspect
import logging
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
from fastmcp.exceptions import AuthorizationError
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from fastmcp.server.auth import AccessToken
from fastmcp.tools.base import Tool
from fastmcp.utilities.components import FastMCPComponent
@dataclass
class AuthContext:
"""Context passed to auth check callables.
Attributes:
token: The current access token, or None if unauthenticated.
component: The tool, resource, resource template, or prompt being accessed.
tool: Backwards-compatible alias for component when it is a Tool.
"""
token: AccessToken | None
component: FastMCPComponent
@property
def tool(self) -> Tool | None:
"""Backwards-compatible access to the component as a Tool."""
from fastmcp.tools.base import Tool
return self.component if isinstance(self.component, Tool) else None
AuthCheck = Callable[[AuthContext], bool] | Callable[[AuthContext], Awaitable[bool]]
def require_scopes(*scopes: str) -> AuthCheck:
"""Require all of the given OAuth scopes."""
required = set(scopes)
def check(ctx: AuthContext) -> bool:
if ctx.token is None:
return False
return required.issubset(set(ctx.token.scopes))
return check
def restrict_tag(tag: str, *, scopes: list[str]) -> AuthCheck:
"""Require scopes when the accessed component has a specific tag."""
required = set(scopes)
def check(ctx: AuthContext) -> bool:
if tag not in ctx.component.tags:
return True
if ctx.token is None:
return False
return required.issubset(set(ctx.token.scopes))
return check
async def run_auth_checks(
checks: AuthCheck | list[AuthCheck],
ctx: AuthContext,
) -> bool:
"""Run auth checks with AND logic."""
check_list = [checks] if not isinstance(checks, list) else checks
check_list = cast(list[AuthCheck], check_list)
for check in check_list:
try:
result = check(ctx)
if inspect.isawaitable(result):
result = await result
if not result:
return False
except AuthorizationError:
raise
except Exception:
logger.warning(
f"Auth check {getattr(check, '__name__', repr(check))} "
"raised an unexpected exception",
exc_info=True,
)
return False
return True

View file

@ -7,7 +7,7 @@ from mcp.types import Icon
from pydantic import BeforeValidator, Field
from typing_extensions import Self, TypeVar
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.utilities.tasks import TaskConfig
from fastmcp.utilities.types import FastMCPBaseModel
if TYPE_CHECKING:

View file

@ -0,0 +1,80 @@
"""Task configuration primitives for FastMCP components."""
from __future__ import annotations
import functools
import inspect
from collections.abc import Callable
from dataclasses import dataclass
from datetime import timedelta
from typing import Any, Literal
from fastmcp.utilities.async_utils import is_coroutine_function
TaskMode = Literal["forbidden", "optional", "required"]
DEFAULT_POLL_INTERVAL = timedelta(seconds=5)
DEFAULT_POLL_INTERVAL_MS = int(DEFAULT_POLL_INTERVAL.total_seconds() * 1000)
DEFAULT_TTL_MS = 60_000
@dataclass
class TaskMeta:
"""Metadata for task-augmented execution requests.
Attributes:
ttl: Client-requested TTL in milliseconds. If None, uses server default.
fn_key: Docket routing key. Auto-derived from component name if None.
"""
ttl: int | None = None
fn_key: str | None = None
@dataclass
class TaskConfig:
"""Configuration for MCP background task execution.
Controls how a component handles task-augmented requests:
- ``forbidden``: Component does not support task execution.
- ``optional``: Component supports both synchronous and task execution.
- ``required``: Component requires task execution.
"""
mode: TaskMode = "optional"
poll_interval: timedelta = DEFAULT_POLL_INTERVAL
@classmethod
def from_bool(cls, value: bool) -> TaskConfig:
"""Convert a boolean task flag to a TaskConfig."""
return cls(mode="optional" if value else "forbidden")
def supports_tasks(self) -> bool:
"""Check if this component supports task execution."""
return self.mode != "forbidden"
def validate_function(self, fn: Callable[..., Any], name: str) -> None:
"""Validate that a function is compatible with this task config."""
if not self.supports_tasks():
return
from fastmcp.server.dependencies import require_docket
require_docket(f"`task=True` on function '{name}'")
fn_to_check = fn
if (
not inspect.isroutine(fn)
and not isinstance(fn, functools.partial)
and callable(fn)
):
fn_to_check = fn.__call__
if isinstance(fn_to_check, staticmethod):
fn_to_check = fn_to_check.__func__
if not is_coroutine_function(fn_to_check):
raise ValueError(
f"'{name}' uses a sync function but has task execution enabled. "
"Background tasks require async functions."
)

View file

@ -5,6 +5,8 @@ to a server. Functions can be added explicitly via server.add_tool() or
discovered by FileSystemProvider.
"""
import subprocess
import sys
from typing import cast
import pytest
@ -16,6 +18,27 @@ from fastmcp.tools.base import Tool
from fastmcp.tools.function_tool import DecoratedTool, FunctionTool, ToolMeta
@pytest.mark.parametrize(
"statement",
[
"from fastmcp.tools import tool",
"from fastmcp.resources import Resource, resource",
"from fastmcp.prompts import Prompt, prompt",
"import sys; import fastmcp.apps.config; assert 'fastmcp.tools.function_tool' not in sys.modules",
"from fastmcp.server.auth.authorization import AuthCheck",
"from fastmcp.server import Context, FastMCP, create_proxy",
],
)
def test_component_import_works_in_fresh_interpreter(statement: str):
result = subprocess.run(
[sys.executable, "-c", statement],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
class TestToolDecorator:
"""Tests for the @tool decorator."""