mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-28 10:18:08 +02:00
Merge pull request #4453 from PrefectHQ/remove/deprecated-params
Remove 3.x deprecated parameters and object-mode decorators
This commit is contained in:
commit
7832f884c6
21 changed files with 77 additions and 970 deletions
|
|
@ -75,7 +75,7 @@ BREAKING CHANGES (will crash at import or runtime):
|
|||
9. ENV VAR: FASTMCP_SHOW_CLI_BANNER renamed to FASTMCP_SHOW_SERVER_BANNER.
|
||||
|
||||
10. DECORATORS: @mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name, .description, or other component attributes on the decorated result will crash with AttributeError.
|
||||
Fix: set FASTMCP_DECORATOR_MODE=object for v2 compat (itself deprecated).
|
||||
Fix: access component objects via the server (e.g. await mcp.get_tool("name")) instead of the decorated function. The FASTMCP_DECORATOR_MODE=object escape hatch that existed in v3 was removed in FastMCP 4.0.
|
||||
|
||||
11. OAUTH STORAGE: Default OAuth client storage changed from DiskStore to FileTreeStore due to pickle deserialization vulnerability in diskcache (CVE-2025-69872). Clients using default storage will re-register automatically on first connection. If using DiskStore explicitly, switch to FileTreeStore (with key/collection sanitization strategies) or add pip install 'py-key-value-aio[disk]'.
|
||||
|
||||
|
|
@ -317,7 +317,7 @@ def greet(name: str) -> str:
|
|||
greet("World") # Works! Returns "Hello, World!"
|
||||
```
|
||||
|
||||
If you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), set `FASTMCP_DECORATOR_MODE=object` for v2 compatibility. This escape hatch is itself deprecated and will be removed in a future release.
|
||||
If you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), the v2-compatible object-returning behavior was available in v3 via `FASTMCP_DECORATOR_MODE=object`. That escape hatch was removed in FastMCP 4.0 — decorators always return the original function now.
|
||||
|
||||
**Background tasks require optional dependency**
|
||||
|
||||
|
|
|
|||
|
|
@ -97,5 +97,4 @@ When setting Docket values in a `.env` file, use a **double** underscore: `FASTM
|
|||
| `FASTMCP_HOME` | `Path` | Platform default | Data directory for FastMCP. Defaults to the platform-specific user data directory. |
|
||||
| `FASTMCP_ENV_FILE` | `str` | `.env` | Path to the `.env` file to load settings from. Must be set as an environment variable (see above). |
|
||||
| `FASTMCP_SERVER_DEPENDENCIES` | `list[str]` | `[]` | Additional dependencies to install in the server environment. |
|
||||
| `FASTMCP_DECORATOR_MODE` | `Literal["function", "object"]` | `function` | Controls what `@tool`, `@resource`, and `@prompt` decorators return. `function` returns the original function (default); `object` returns component objects (deprecated, will be removed). |
|
||||
| `FASTMCP_TEST_MODE` | `bool` | `false` | Enable test mode. |
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
"""Provides a base mixin class and decorators for easy registration of class methods with FastMCP."""
|
||||
|
||||
import inspect
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
from fastmcp.prompts.base import Prompt
|
||||
from fastmcp.resources.base import Resource
|
||||
from fastmcp.tools.base import Tool
|
||||
|
|
@ -73,15 +70,6 @@ def mcp_tool(
|
|||
f"Valid keyword arguments are: {sorted(_TOOL_VALID_KWARGS)}"
|
||||
)
|
||||
|
||||
if "serializer" in kwargs and fastmcp.settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"The `serializer` parameter is deprecated. "
|
||||
"Return ToolResult from your tools for full control over serialization. "
|
||||
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
call_args: dict[str, Any] = {"name": name or get_fn_name(func), **kwargs}
|
||||
if enabled is not None:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload
|
||||
|
||||
|
|
@ -29,7 +28,6 @@ from mcp_types import PromptArgument as SDKPromptArgument
|
|||
from pydantic import Field
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
from fastmcp.utilities.authorization import AuthCheck
|
||||
from fastmcp.utilities.components import FastMCPComponent
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
|
@ -414,27 +412,3 @@ __all__ = [
|
|||
"PromptArgument",
|
||||
"PromptResult",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
"""Deprecated re-exports for backwards compatibility."""
|
||||
deprecated_exports = {
|
||||
"FunctionPrompt": "FunctionPrompt",
|
||||
"prompt": "prompt",
|
||||
}
|
||||
|
||||
if name in deprecated_exports:
|
||||
import fastmcp
|
||||
|
||||
if fastmcp.settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
f"Importing {name} from fastmcp.prompts.prompt is deprecated. "
|
||||
f"Import from fastmcp.prompts.function_prompt instead.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
from fastmcp.prompts import function_prompt
|
||||
|
||||
return getattr(function_prompt, name)
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
|||
import functools
|
||||
import inspect
|
||||
import json
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from types import MethodType
|
||||
|
|
@ -24,9 +23,7 @@ import pydantic_core
|
|||
from mcp_types import Icon
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.decorators import resolve_task_config
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning, FastMCPError, PromptError
|
||||
from fastmcp.exceptions import FastMCPError, PromptError
|
||||
from fastmcp.prompts.base import Prompt, PromptArgument, PromptResult
|
||||
from fastmcp.utilities.async_utils import (
|
||||
call_sync_fn_in_threadpool,
|
||||
|
|
@ -457,23 +454,6 @@ def prompt(
|
|||
"See https://gofastmcp.com/servers/prompts#using-with-methods"
|
||||
)
|
||||
|
||||
def create_prompt(
|
||||
fn: Callable[..., Any], prompt_name: str | None
|
||||
) -> FunctionPrompt:
|
||||
# Create metadata first, then pass it
|
||||
prompt_meta = PromptMeta(
|
||||
name=prompt_name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
meta=meta,
|
||||
task=resolve_task_config(task),
|
||||
auth=auth,
|
||||
)
|
||||
return FunctionPrompt.from_function(fn, metadata=prompt_meta)
|
||||
|
||||
def attach_metadata(fn: F, prompt_name: str | None) -> F:
|
||||
metadata = PromptMeta(
|
||||
name=prompt_name,
|
||||
|
|
@ -491,14 +471,6 @@ def prompt(
|
|||
return fn
|
||||
|
||||
def decorator(fn: F, prompt_name: str | None) -> F:
|
||||
if fastmcp.settings.decorator_mode == "object":
|
||||
warnings.warn(
|
||||
"decorator_mode='object' is deprecated and will be removed in a future version. "
|
||||
"Decorators now return the original function with metadata attached.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
return create_prompt(fn, prompt_name) # type: ignore[return-value] # ty:ignore[invalid-return-type]
|
||||
return attach_metadata(fn, prompt_name)
|
||||
|
||||
if inspect.isroutine(name_or_fn):
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ from pydantic import (
|
|||
from pydantic.json_schema import SkipJsonSchema
|
||||
from typing_extensions import Self
|
||||
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
from fastmcp.utilities.authorization import AuthCheck
|
||||
from fastmcp.utilities.components import FastMCPComponent
|
||||
from fastmcp.utilities.tasks import TaskConfig, TaskMeta
|
||||
|
|
@ -469,29 +468,3 @@ __all__ = [
|
|||
"ResourceContent",
|
||||
"ResourceResult",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
"""Deprecated re-exports for backwards compatibility."""
|
||||
deprecated_exports = {
|
||||
"FunctionResource": "FunctionResource",
|
||||
"resource": "resource",
|
||||
}
|
||||
|
||||
if name in deprecated_exports:
|
||||
import warnings
|
||||
|
||||
import fastmcp
|
||||
|
||||
if fastmcp.settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
f"Importing {name} from fastmcp.resources.resource is deprecated. "
|
||||
f"Import from fastmcp.resources.function_resource instead.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
from fastmcp.resources import function_resource
|
||||
|
||||
return getattr(function_resource, name)
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
|||
|
||||
import functools
|
||||
import inspect
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from types import MethodType
|
||||
|
|
@ -22,9 +21,6 @@ from mcp_types import Annotations, Icon
|
|||
from pydantic import AnyUrl
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.decorators import resolve_task_config
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
from fastmcp.resources.base import Resource, ResourceResult
|
||||
from fastmcp.utilities.async_utils import (
|
||||
call_sync_fn_in_threadpool,
|
||||
|
|
@ -37,7 +33,6 @@ from fastmcp.utilities.tasks import TaskConfig
|
|||
if TYPE_CHECKING:
|
||||
from docket import Docket
|
||||
|
||||
from fastmcp.resources.template import ResourceTemplate
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
|
@ -275,51 +270,6 @@ def resource(
|
|||
"Use @resource('uri') instead of @resource"
|
||||
)
|
||||
|
||||
def create_resource(fn: Callable[..., Any]) -> FunctionResource | ResourceTemplate:
|
||||
from fastmcp.resources.template import ResourceTemplate
|
||||
from fastmcp.server.dependencies import without_injected_parameters
|
||||
|
||||
resolved = resolve_task_config(task)
|
||||
has_uri_params = "{" in uri and "}" in uri
|
||||
wrapper_fn = without_injected_parameters(fn)
|
||||
has_func_params = bool(inspect.signature(wrapper_fn).parameters)
|
||||
|
||||
# Create metadata first
|
||||
resource_meta = ResourceMeta(
|
||||
uri=uri,
|
||||
name=name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
mime_type=mime_type,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
task=resolved,
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
if has_uri_params or has_func_params:
|
||||
# ResourceTemplate doesn't have metadata support yet, so pass individual params
|
||||
return ResourceTemplate.from_function(
|
||||
fn=fn,
|
||||
uri_template=uri,
|
||||
name=name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
mime_type=mime_type,
|
||||
tags=tags,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
task=resolved,
|
||||
auth=auth,
|
||||
)
|
||||
else:
|
||||
return FunctionResource.from_function(fn, metadata=resource_meta)
|
||||
|
||||
def attach_metadata(fn: F) -> F:
|
||||
metadata = ResourceMeta(
|
||||
uri=uri,
|
||||
|
|
@ -340,14 +290,6 @@ def resource(
|
|||
return fn
|
||||
|
||||
def decorator(fn: F) -> F:
|
||||
if fastmcp.settings.decorator_mode == "object":
|
||||
warnings.warn(
|
||||
"decorator_mode='object' is deprecated and will be removed in a future version. "
|
||||
"Decorators now return the original function with metadata attached.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return create_resource(fn) # type: ignore[return-value] # ty:ignore[invalid-return-type]
|
||||
return attach_metadata(fn)
|
||||
|
||||
return decorator
|
||||
|
|
|
|||
|
|
@ -343,8 +343,6 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
|
|||
annotations=meta.annotations,
|
||||
meta=meta.meta,
|
||||
task=resolved_task,
|
||||
exclude_args=meta.exclude_args,
|
||||
serializer=meta.serializer,
|
||||
timeout=meta.timeout,
|
||||
auth=meta.auth,
|
||||
run_in_thread=meta.run_in_thread,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|||
|
||||
import mcp_types
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.prompts.base import Prompt
|
||||
from fastmcp.prompts.function_prompt import FunctionPrompt
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
|
|
@ -189,44 +188,24 @@ class PromptDecoratorMixin:
|
|||
f"See https://gofastmcp.com/servers/prompts#using-with-methods"
|
||||
)
|
||||
|
||||
resolved_task: bool | TaskConfig = task if task is not None else False
|
||||
from fastmcp.prompts.function_prompt import PromptMeta
|
||||
|
||||
if fastmcp.settings.decorator_mode == "object":
|
||||
prompt_obj = Prompt.from_function(
|
||||
fn,
|
||||
name=prompt_name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
meta=meta,
|
||||
task=resolved_task,
|
||||
auth=auth,
|
||||
)
|
||||
self._add_component(prompt_obj)
|
||||
if not enabled:
|
||||
self.disable(keys={prompt_obj.key})
|
||||
return prompt_obj
|
||||
else:
|
||||
from fastmcp.prompts.function_prompt import PromptMeta
|
||||
|
||||
metadata = PromptMeta(
|
||||
name=prompt_name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
meta=meta,
|
||||
task=task,
|
||||
auth=auth,
|
||||
enabled=enabled,
|
||||
)
|
||||
target = fn.__func__ if hasattr(fn, "__func__") else fn
|
||||
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
self.add_prompt(fn)
|
||||
return fn
|
||||
metadata = PromptMeta(
|
||||
name=prompt_name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
meta=meta,
|
||||
task=task,
|
||||
auth=auth,
|
||||
enabled=enabled,
|
||||
)
|
||||
target = fn.__func__ if hasattr(fn, "__func__") else fn
|
||||
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
self.add_prompt(fn)
|
||||
return fn
|
||||
|
||||
if inspect.isroutine(name_or_fn):
|
||||
return decorate_and_register(name_or_fn, name)
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ from typing import TYPE_CHECKING, Any, TypeVar
|
|||
import mcp_types
|
||||
from mcp_types import Annotations
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.resources.base import Resource
|
||||
from fastmcp.resources.function_resource import resource as standalone_resource
|
||||
from fastmcp.resources.template import ResourceTemplate
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.tasks.config import TaskConfig
|
||||
|
|
@ -167,8 +165,6 @@ class ResourceDecoratorMixin:
|
|||
"Use @resource('uri') instead of @resource"
|
||||
)
|
||||
|
||||
resolved_task: bool | TaskConfig = task if task is not None else False
|
||||
|
||||
def decorator(fn: AnyFunction) -> Any:
|
||||
# Check for unbound method
|
||||
try:
|
||||
|
|
@ -190,54 +186,26 @@ class ResourceDecoratorMixin:
|
|||
f"See https://gofastmcp.com/servers/resources#using-with-methods"
|
||||
)
|
||||
|
||||
if fastmcp.settings.decorator_mode == "object":
|
||||
create_resource = standalone_resource(
|
||||
uri,
|
||||
name=name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
mime_type=mime_type,
|
||||
tags=tags,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
task=resolved_task,
|
||||
auth=auth,
|
||||
)
|
||||
obj = create_resource(fn)
|
||||
# In legacy mode, standalone_resource always returns a component
|
||||
assert isinstance(obj, (Resource, ResourceTemplate))
|
||||
if isinstance(obj, ResourceTemplate):
|
||||
self.add_template(obj)
|
||||
if not enabled:
|
||||
self.disable(keys={obj.key})
|
||||
else:
|
||||
self.add_resource(obj)
|
||||
if not enabled:
|
||||
self.disable(keys={obj.key})
|
||||
return obj
|
||||
else:
|
||||
from fastmcp.resources.function_resource import ResourceMeta
|
||||
from fastmcp.resources.function_resource import ResourceMeta
|
||||
|
||||
metadata = ResourceMeta(
|
||||
uri=uri,
|
||||
name=name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
mime_type=mime_type,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
task=task,
|
||||
auth=auth,
|
||||
enabled=enabled,
|
||||
)
|
||||
target = fn.__func__ if hasattr(fn, "__func__") else fn
|
||||
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
self.add_resource(fn)
|
||||
return fn
|
||||
metadata = ResourceMeta(
|
||||
uri=uri,
|
||||
name=name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
mime_type=mime_type,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
task=task,
|
||||
auth=auth,
|
||||
enabled=enabled,
|
||||
)
|
||||
target = fn.__func__ if hasattr(fn, "__func__") else fn
|
||||
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
self.add_resource(fn)
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from __future__ import annotations
|
|||
|
||||
import inspect
|
||||
import types
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
from typing import (
|
||||
|
|
@ -26,8 +25,6 @@ from typing import (
|
|||
import mcp_types
|
||||
from mcp_types import ToolAnnotations
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
from fastmcp.server.auth.authorization import AuthCheck
|
||||
from fastmcp.server.tasks.config import TaskConfig
|
||||
from fastmcp.tools.base import Tool
|
||||
|
|
@ -44,7 +41,6 @@ except ImportError:
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp.server.providers.local_provider import LocalProvider
|
||||
from fastmcp.tools.base import ToolResultSerializerType
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
|
@ -161,8 +157,6 @@ class ToolDecoratorMixin:
|
|||
annotations=fmeta.annotations,
|
||||
meta=tool_meta,
|
||||
task=resolved_task,
|
||||
exclude_args=fmeta.exclude_args,
|
||||
serializer=fmeta.serializer,
|
||||
timeout=fmeta.timeout,
|
||||
auth=fmeta.auth,
|
||||
run_in_thread=fmeta.run_in_thread,
|
||||
|
|
@ -188,11 +182,9 @@ class ToolDecoratorMixin:
|
|||
tags: set[str] | None = None,
|
||||
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
||||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
enabled: bool = True,
|
||||
task: bool | TaskConfig | None = None,
|
||||
serializer: ToolResultSerializerType | None = None, # Deprecated
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
run_in_thread: bool = True,
|
||||
|
|
@ -211,20 +203,17 @@ class ToolDecoratorMixin:
|
|||
tags: set[str] | None = None,
|
||||
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
||||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
enabled: bool = True,
|
||||
task: bool | TaskConfig | None = None,
|
||||
serializer: ToolResultSerializerType | None = None, # Deprecated
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
run_in_thread: bool = True,
|
||||
) -> Callable[[F], F]: ...
|
||||
|
||||
# NOTE: This method mirrors fastmcp.tools.tool() but adds registration,
|
||||
# the `enabled` param, and supports deprecated params (serializer, exclude_args).
|
||||
# When deprecated params are removed, this should delegate to the standalone
|
||||
# decorator to reduce duplication.
|
||||
# NOTE: This method mirrors fastmcp.tools.tool() but adds registration and
|
||||
# the `enabled` param. It could delegate to the standalone decorator to
|
||||
# reduce duplication.
|
||||
def tool(
|
||||
self: LocalProvider,
|
||||
name_or_fn: str | AnyFunction | None = None,
|
||||
|
|
@ -237,11 +226,9 @@ class ToolDecoratorMixin:
|
|||
tags: set[str] | None = None,
|
||||
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
||||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
enabled: bool = True,
|
||||
task: bool | TaskConfig | None = None,
|
||||
serializer: ToolResultSerializerType | None = None, # Deprecated
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
run_in_thread: bool = True,
|
||||
|
|
@ -268,11 +255,9 @@ class ToolDecoratorMixin:
|
|||
tags: Optional set of tags for categorizing the tool
|
||||
output_schema: Optional JSON schema for the tool's output
|
||||
annotations: Optional annotations about the tool's behavior
|
||||
exclude_args: Optional list of argument names to exclude from the tool schema
|
||||
meta: Optional meta information about the tool
|
||||
enabled: Whether the tool is enabled (default True). If False, adds to blocklist.
|
||||
task: Optional task configuration for background execution
|
||||
serializer: Deprecated. Return ToolResult from your tools for full control over serialization.
|
||||
|
||||
Returns:
|
||||
The registered FunctionTool or a decorator function.
|
||||
|
|
@ -290,14 +275,6 @@ class ToolDecoratorMixin:
|
|||
return str(x)
|
||||
```
|
||||
"""
|
||||
if serializer is not None and fastmcp.settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"The `serializer` parameter is deprecated. "
|
||||
"Return ToolResult from your tools for full control over serialization. "
|
||||
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if isinstance(annotations, dict):
|
||||
annotations = ToolAnnotations(**annotations)
|
||||
|
||||
|
|
@ -330,57 +307,28 @@ class ToolDecoratorMixin:
|
|||
f"See https://gofastmcp.com/servers/tools#using-with-methods"
|
||||
)
|
||||
|
||||
resolved_task: bool | TaskConfig = task if task is not None else False
|
||||
from fastmcp.tools.function_tool import ToolMeta
|
||||
|
||||
if fastmcp.settings.decorator_mode == "object":
|
||||
tool_obj = Tool.from_function(
|
||||
fn,
|
||||
name=tool_name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
output_schema=output_schema,
|
||||
annotations=annotations,
|
||||
exclude_args=exclude_args,
|
||||
meta=meta,
|
||||
serializer=serializer,
|
||||
task=resolved_task,
|
||||
timeout=timeout,
|
||||
auth=auth,
|
||||
run_in_thread=run_in_thread,
|
||||
)
|
||||
self._add_component(tool_obj)
|
||||
if not enabled:
|
||||
self.disable(keys={tool_obj.key})
|
||||
_maybe_apply_prefab_ui(self, tool_obj)
|
||||
return tool_obj
|
||||
else:
|
||||
from fastmcp.tools.function_tool import ToolMeta
|
||||
|
||||
metadata = ToolMeta(
|
||||
name=tool_name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
output_schema=output_schema,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
task=task,
|
||||
exclude_args=exclude_args,
|
||||
serializer=serializer,
|
||||
timeout=timeout,
|
||||
auth=auth,
|
||||
enabled=enabled,
|
||||
run_in_thread=run_in_thread,
|
||||
)
|
||||
target = fn.__func__ if hasattr(fn, "__func__") else fn
|
||||
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
tool_obj = self.add_tool(fn)
|
||||
return fn
|
||||
metadata = ToolMeta(
|
||||
name=tool_name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
output_schema=output_schema,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
task=task,
|
||||
timeout=timeout,
|
||||
auth=auth,
|
||||
enabled=enabled,
|
||||
run_in_thread=run_in_thread,
|
||||
)
|
||||
target = fn.__func__ if hasattr(fn, "__func__") else fn
|
||||
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
self.add_tool(fn)
|
||||
return fn
|
||||
|
||||
if inspect.isroutine(name_or_fn):
|
||||
return decorate_and_register(name_or_fn, name)
|
||||
|
|
@ -412,11 +360,9 @@ class ToolDecoratorMixin:
|
|||
tags=tags,
|
||||
output_schema=output_schema,
|
||||
annotations=annotations,
|
||||
exclude_args=exclude_args,
|
||||
meta=meta,
|
||||
enabled=enabled,
|
||||
task=task,
|
||||
serializer=serializer,
|
||||
timeout=timeout,
|
||||
auth=auth,
|
||||
run_in_thread=run_in_thread,
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import re
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
from mcp_types import ToolAnnotations
|
||||
from pydantic.networks import AnyUrl
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
from fastmcp.resources import (
|
||||
Resource,
|
||||
ResourceContent,
|
||||
|
|
@ -151,16 +147,7 @@ class OpenAPITool(Tool):
|
|||
output_schema: dict[str, Any] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
serializer: Callable[[Any], str] | None = None, # Deprecated
|
||||
):
|
||||
if serializer is not None and fastmcp.settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"The `serializer` parameter is deprecated. "
|
||||
"Return ToolResult from your tools for full control over serialization. "
|
||||
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(
|
||||
name=name,
|
||||
description=description,
|
||||
|
|
@ -168,7 +155,6 @@ class OpenAPITool(Tool):
|
|||
output_schema=output_schema,
|
||||
tags=tags or set(),
|
||||
annotations=annotations,
|
||||
serializer=serializer,
|
||||
)
|
||||
self._client = client
|
||||
self._route = route
|
||||
|
|
|
|||
|
|
@ -1649,7 +1649,6 @@ class FastMCP(
|
|||
tags: set[str] | None = None,
|
||||
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
||||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
app: AppConfig | dict[str, Any] | bool | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
|
|
@ -1671,7 +1670,6 @@ class FastMCP(
|
|||
tags: set[str] | None = None,
|
||||
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
||||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
app: AppConfig | dict[str, Any] | bool | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
|
|
@ -1692,7 +1690,6 @@ class FastMCP(
|
|||
tags: set[str] | None = None,
|
||||
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
||||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
app: AppConfig | dict[str, Any] | bool | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
|
|
@ -1724,8 +1721,6 @@ class FastMCP(
|
|||
tags: Optional set of tags for categorizing the tool
|
||||
output_schema: Optional JSON schema for the tool's output
|
||||
annotations: Optional annotations about the tool's behavior
|
||||
exclude_args: Optional list of argument names to exclude from the tool schema.
|
||||
Deprecated: Use `Depends()` for dependency injection instead.
|
||||
meta: Optional meta information about the tool
|
||||
|
||||
Examples:
|
||||
|
|
@ -1771,7 +1766,6 @@ class FastMCP(
|
|||
tags=tags,
|
||||
output_schema=output_schema,
|
||||
annotations=annotations,
|
||||
exclude_args=exclude_args,
|
||||
meta=meta,
|
||||
task=task if task is not None else self._support_tasks_by_default,
|
||||
timeout=timeout,
|
||||
|
|
|
|||
|
|
@ -396,20 +396,3 @@ class Settings(BaseSettings):
|
|||
),
|
||||
),
|
||||
] = "stable"
|
||||
|
||||
decorator_mode: Annotated[
|
||||
Literal["function", "object"],
|
||||
Field(
|
||||
description=inspect.cleandoc(
|
||||
"""
|
||||
Controls what decorators (@tool, @resource, @prompt) return.
|
||||
|
||||
- "function" (default): Decorators return the original function unchanged.
|
||||
The function remains callable and is registered with the server normally.
|
||||
- "object" (deprecated): Decorators return component objects (FunctionTool,
|
||||
FunctionResource, FunctionPrompt). This was the default behavior in v2 and
|
||||
will be removed in a future version.
|
||||
"""
|
||||
),
|
||||
),
|
||||
] = "function"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
ClassVar,
|
||||
TypeAlias,
|
||||
overload,
|
||||
)
|
||||
|
||||
|
|
@ -26,7 +24,6 @@ from mcp_types import Tool as MCPTool
|
|||
from pydantic import BaseModel, Field, model_validator
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
from fastmcp.utilities.authorization import AuthCheck
|
||||
from fastmcp.utilities.components import FastMCPComponent
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
|
@ -59,9 +56,6 @@ if TYPE_CHECKING:
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
ToolResultSerializerType: TypeAlias = Callable[[Any], str]
|
||||
|
||||
|
||||
def resolve_serialize_by_alias(value: Any) -> bool:
|
||||
"""Resolve the effective ``by_alias`` setting for serializing *value*.
|
||||
|
||||
|
|
@ -196,12 +190,6 @@ class Tool(FastMCPComponent):
|
|||
ToolExecution | None,
|
||||
Field(description="Task execution configuration (SEP-1686)"),
|
||||
] = None
|
||||
serializer: Annotated[
|
||||
SkipJsonSchema[ToolResultSerializerType | None],
|
||||
Field(
|
||||
description="Deprecated. Return ToolResult from your tools for full control over serialization."
|
||||
),
|
||||
] = None
|
||||
auth: Annotated[
|
||||
SkipJsonSchema[AuthCheck | list[AuthCheck] | None],
|
||||
Field(description="Authorization checks for this tool", exclude=True),
|
||||
|
|
@ -266,9 +254,7 @@ class Tool(FastMCPComponent):
|
|||
icons: list[Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
||||
serializer: ToolResultSerializerType | None = None, # Deprecated
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
timeout: float | None = None,
|
||||
|
|
@ -287,9 +273,7 @@ class Tool(FastMCPComponent):
|
|||
icons=icons,
|
||||
tags=tags,
|
||||
annotations=annotations,
|
||||
exclude_args=exclude_args,
|
||||
output_schema=output_schema,
|
||||
serializer=serializer,
|
||||
meta=meta,
|
||||
task=task,
|
||||
timeout=timeout,
|
||||
|
|
@ -313,7 +297,7 @@ class Tool(FastMCPComponent):
|
|||
"""Convert a raw result to ToolResult.
|
||||
|
||||
Handles ToolResult passthrough and converts raw values using the tool's
|
||||
attributes (serializer, output_schema) for proper conversion.
|
||||
attributes (output_schema) for proper conversion.
|
||||
"""
|
||||
if isinstance(raw_value, ToolResult):
|
||||
return raw_value
|
||||
|
|
@ -330,7 +314,7 @@ class Tool(FastMCPComponent):
|
|||
fastmcp_app_name=_get_fastmcp_app_name(self),
|
||||
)
|
||||
|
||||
content = _convert_to_content(raw_value, serializer=self.serializer)
|
||||
content = _convert_to_content(raw_value)
|
||||
|
||||
# Bytes can't be represented as structured JSON content
|
||||
if isinstance(raw_value, bytes):
|
||||
|
|
@ -460,7 +444,6 @@ class Tool(FastMCPComponent):
|
|||
tags: set[str] | None = None,
|
||||
annotations: ToolAnnotations | NotSetT | None = NotSet,
|
||||
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
||||
serializer: ToolResultSerializerType | None = None, # Deprecated
|
||||
meta: dict[str, Any] | NotSetT | None = NotSet,
|
||||
transform_args: dict[str, ArgTransform] | None = None,
|
||||
transform_fn: Callable[..., Any] | None = None,
|
||||
|
|
@ -479,7 +462,6 @@ class Tool(FastMCPComponent):
|
|||
tags=tags,
|
||||
annotations=annotations,
|
||||
output_schema=output_schema,
|
||||
serializer=serializer,
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
|
|
@ -505,25 +487,8 @@ class Tool(FastMCPComponent):
|
|||
}
|
||||
|
||||
|
||||
def _serialize_with_fallback(
|
||||
result: Any, serializer: ToolResultSerializerType | None = None
|
||||
) -> str:
|
||||
if serializer is not None:
|
||||
try:
|
||||
return serializer(result)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Error serializing tool result: %s",
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return default_serializer(result)
|
||||
|
||||
|
||||
def _convert_to_single_content_block(
|
||||
item: Any,
|
||||
serializer: ToolResultSerializerType | None = None,
|
||||
) -> ContentBlock:
|
||||
if isinstance(item, ContentBlock):
|
||||
return item
|
||||
|
|
@ -548,7 +513,7 @@ def _convert_to_single_content_block(
|
|||
|
||||
return TextContent(type="text", text=base64.b64encode(item).decode("ascii"))
|
||||
|
||||
return TextContent(type="text", text=_serialize_with_fallback(item, serializer))
|
||||
return TextContent(type="text", text=default_serializer(item))
|
||||
|
||||
|
||||
_PREFAB_TEXT_FALLBACK = "[Rendered Prefab UI]"
|
||||
|
|
@ -599,7 +564,6 @@ def _prefab_to_tool_result(app: Any, fastmcp_app_name: str | None = None) -> Too
|
|||
|
||||
def _convert_to_content(
|
||||
result: Any,
|
||||
serializer: ToolResultSerializerType | None = None,
|
||||
) -> list[ContentBlock]:
|
||||
"""Convert a result to a sequence of content objects."""
|
||||
|
||||
|
|
@ -607,7 +571,7 @@ def _convert_to_content(
|
|||
return []
|
||||
|
||||
if not isinstance(result, (list | tuple)):
|
||||
return [_convert_to_single_content_block(result, serializer)]
|
||||
return [_convert_to_single_content_block(result)]
|
||||
|
||||
# If all items are ContentBlocks, return them as is
|
||||
if all(isinstance(item, ContentBlock) for item in result):
|
||||
|
|
@ -617,38 +581,13 @@ def _convert_to_content(
|
|||
# without aggregating them
|
||||
if any(isinstance(item, ContentBlock | Image | Audio | File) for item in result):
|
||||
return [
|
||||
_convert_to_single_content_block(item, serializer)
|
||||
_convert_to_single_content_block(item)
|
||||
if not isinstance(item, ContentBlock)
|
||||
else item
|
||||
for item in result
|
||||
]
|
||||
# If none of the items are ContentBlocks, aggregate all items into a single TextContent
|
||||
return [TextContent(type="text", text=_serialize_with_fallback(result, serializer))]
|
||||
return [TextContent(type="text", text=default_serializer(result))]
|
||||
|
||||
|
||||
__all__ = ["Tool", "ToolResult"]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
"""Deprecated re-exports for backwards compatibility."""
|
||||
deprecated_exports = {
|
||||
"FunctionTool": "FunctionTool",
|
||||
"ParsedFunction": "ParsedFunction",
|
||||
"tool": "tool",
|
||||
}
|
||||
|
||||
if name in deprecated_exports:
|
||||
import fastmcp
|
||||
|
||||
if fastmcp.settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
f"Importing {name} from fastmcp.tools.tool is deprecated. "
|
||||
f"Import from fastmcp.tools.function_tool instead.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
from fastmcp.tools import function_tool
|
||||
|
||||
return getattr(function_tool, name)
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ from fastmcp.utilities.types import (
|
|||
Audio,
|
||||
File,
|
||||
Image,
|
||||
create_function_without_params,
|
||||
get_cached_typeadapter,
|
||||
is_class_member_of_type,
|
||||
replace_type,
|
||||
|
|
@ -178,7 +177,6 @@ class ParsedFunction:
|
|||
def from_function(
|
||||
cls,
|
||||
fn: Callable[..., Any],
|
||||
exclude_args: list[str] | None = None,
|
||||
validate: bool = True,
|
||||
wrap_non_object_output_schema: bool = True,
|
||||
) -> ParsedFunction:
|
||||
|
|
@ -193,19 +191,6 @@ class ParsedFunction:
|
|||
"Functions with **kwargs are not supported as tools"
|
||||
)
|
||||
|
||||
# Reject exclude_args that don't exist in the function or don't have a default value
|
||||
if exclude_args:
|
||||
for arg_name in exclude_args:
|
||||
if arg_name not in sig.parameters:
|
||||
raise ValueError(
|
||||
f"Parameter '{arg_name}' in exclude_args does not exist in function."
|
||||
)
|
||||
param = sig.parameters[arg_name]
|
||||
if param.default == inspect.Parameter.empty:
|
||||
raise ValueError(
|
||||
f"Parameter '{arg_name}' in exclude_args must have a default value."
|
||||
)
|
||||
|
||||
# collect name and description before we potentially modify the function
|
||||
fn_name = getattr(fn, "__name__", None) or fn.__class__.__name__
|
||||
outer_docstring = parse_docstring(fn)
|
||||
|
|
@ -241,19 +226,10 @@ class ParsedFunction:
|
|||
# Handle injected parameters (Context, Docket dependencies)
|
||||
wrapper_fn = without_injected_parameters(fn)
|
||||
|
||||
# Also handle exclude_args with non-serializable types (issue #2431)
|
||||
# This must happen before Pydantic tries to serialize the parameters
|
||||
if exclude_args:
|
||||
wrapper_fn = create_function_without_params(wrapper_fn, list(exclude_args))
|
||||
|
||||
input_type_adapter = get_cached_typeadapter(wrapper_fn)
|
||||
input_schema = input_type_adapter.json_schema()
|
||||
|
||||
# Compress and handle exclude_args
|
||||
prune_params = list(exclude_args) if exclude_args else None
|
||||
input_schema = compress_schema(
|
||||
input_schema, prune_params=prune_params, prune_titles=True
|
||||
)
|
||||
input_schema = compress_schema(input_schema, prune_titles=True)
|
||||
|
||||
# Inject parameter descriptions from the docstring into the schema.
|
||||
# Explicit annotations (Field(description=...), Annotated[x, "..."])
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
|||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
|
|
@ -30,13 +29,11 @@ from pydantic import Field, TypeAdapter
|
|||
from pydantic import ValidationError as PydanticValidationError
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.decorators import get_fastmcp_meta, resolve_task_config
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning, ValidationError
|
||||
from fastmcp.decorators import get_fastmcp_meta
|
||||
from fastmcp.exceptions import ValidationError
|
||||
from fastmcp.tools.base import (
|
||||
Tool,
|
||||
ToolResult,
|
||||
ToolResultSerializerType,
|
||||
)
|
||||
from fastmcp.tools.function_parsing import ParsedFunction, _is_object_schema
|
||||
from fastmcp.utilities.async_utils import (
|
||||
|
|
@ -170,8 +167,6 @@ class ToolMeta:
|
|||
meta: dict[str, Any] | None = None
|
||||
app: Any = None
|
||||
task: bool | TaskConfig | None = None
|
||||
exclude_args: list[str] | None = None
|
||||
serializer: Any | None = None
|
||||
timeout: float | None = None
|
||||
auth: AuthCheck | list[AuthCheck] | None = None
|
||||
enabled: bool = True
|
||||
|
|
@ -236,9 +231,7 @@ class FunctionTool(Tool):
|
|||
icons: list[Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
||||
serializer: ToolResultSerializerType | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
timeout: float | None = None,
|
||||
|
|
@ -268,14 +261,12 @@ class FunctionTool(Tool):
|
|||
annotations,
|
||||
meta,
|
||||
task,
|
||||
serializer,
|
||||
timeout,
|
||||
auth,
|
||||
run_in_thread,
|
||||
]
|
||||
)
|
||||
or output_schema is not NotSet
|
||||
or exclude_args is not None
|
||||
)
|
||||
|
||||
if metadata is not None and individual_params_provided:
|
||||
|
|
@ -302,31 +293,12 @@ class FunctionTool(Tool):
|
|||
annotations=annotations,
|
||||
meta=meta,
|
||||
task=task,
|
||||
exclude_args=exclude_args,
|
||||
serializer=serializer,
|
||||
timeout=timeout,
|
||||
auth=auth,
|
||||
run_in_thread=True if run_in_thread is None else run_in_thread,
|
||||
)
|
||||
|
||||
if metadata.serializer is not None and fastmcp.settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"The `serializer` parameter is deprecated. "
|
||||
"Return ToolResult from your tools for full control over serialization. "
|
||||
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if metadata.exclude_args and fastmcp.settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"The `exclude_args` parameter is deprecated as of FastMCP 2.14. "
|
||||
"Use dependency injection with `Depends()` instead for better lifecycle management. "
|
||||
"See https://gofastmcp.com/servers/dependency-injection#using-depends for examples.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
parsed_fn = ParsedFunction.from_function(fn, exclude_args=metadata.exclude_args)
|
||||
parsed_fn = ParsedFunction.from_function(fn)
|
||||
func_name = metadata.name or parsed_fn.name
|
||||
|
||||
if func_name == "<lambda>":
|
||||
|
|
@ -389,7 +361,6 @@ class FunctionTool(Tool):
|
|||
output_schema=final_output_schema,
|
||||
annotations=metadata.annotations,
|
||||
tags=metadata.tags or set(),
|
||||
serializer=metadata.serializer,
|
||||
meta=metadata.meta,
|
||||
task_config=task_config,
|
||||
timeout=metadata.timeout,
|
||||
|
|
@ -605,8 +576,6 @@ def tool(
|
|||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
serializer: Any | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
run_in_thread: bool = True,
|
||||
|
|
@ -625,8 +594,6 @@ def tool(
|
|||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
serializer: Any | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
run_in_thread: bool = True,
|
||||
|
|
@ -646,8 +613,6 @@ def tool(
|
|||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
task: bool | TaskConfig | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
serializer: Any | None = None,
|
||||
timeout: float | None = None,
|
||||
auth: AuthCheck | list[AuthCheck] | None = None,
|
||||
run_in_thread: bool = True,
|
||||
|
|
@ -676,27 +641,6 @@ def tool(
|
|||
"See https://gofastmcp.com/servers/tools#using-with-methods"
|
||||
)
|
||||
|
||||
def create_tool(fn: Callable[..., Any], tool_name: str | None) -> FunctionTool:
|
||||
# Create metadata first, then pass it
|
||||
tool_meta = ToolMeta(
|
||||
name=tool_name,
|
||||
version=version,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
output_schema=output_schema,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
task=resolve_task_config(task),
|
||||
exclude_args=exclude_args,
|
||||
serializer=serializer,
|
||||
timeout=timeout,
|
||||
auth=auth,
|
||||
run_in_thread=run_in_thread,
|
||||
)
|
||||
return FunctionTool.from_function(fn, metadata=tool_meta)
|
||||
|
||||
def attach_metadata(fn: F, tool_name: str | None) -> F:
|
||||
metadata = ToolMeta(
|
||||
name=tool_name,
|
||||
|
|
@ -709,8 +653,6 @@ def tool(
|
|||
annotations=annotations,
|
||||
meta=meta,
|
||||
task=task,
|
||||
exclude_args=exclude_args,
|
||||
serializer=serializer,
|
||||
timeout=timeout,
|
||||
auth=auth,
|
||||
run_in_thread=run_in_thread,
|
||||
|
|
@ -720,14 +662,6 @@ def tool(
|
|||
return fn
|
||||
|
||||
def decorator(fn: F, tool_name: str | None) -> F:
|
||||
if fastmcp.settings.decorator_mode == "object":
|
||||
warnings.warn(
|
||||
"decorator_mode='object' is deprecated and will be removed in a future version. "
|
||||
"Decorators now return the original function with metadata attached.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
return create_tool(fn, tool_name) # type: ignore[return-value] # ty:ignore[invalid-return-type]
|
||||
return attach_metadata(fn, tool_name)
|
||||
|
||||
if inspect.isroutine(name_or_fn):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from contextvars import ContextVar
|
||||
from copy import deepcopy
|
||||
|
|
@ -15,8 +14,6 @@ from pydantic.fields import Field
|
|||
from pydantic.functional_validators import BeforeValidator
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
from fastmcp.tools.base import (
|
||||
Tool,
|
||||
ToolResult,
|
||||
|
|
@ -343,9 +340,7 @@ class TransformedTool(Tool):
|
|||
|
||||
# Otherwise convert to content and create ToolResult with proper structured content
|
||||
|
||||
unstructured_result = _convert_to_content(
|
||||
result, serializer=self.serializer
|
||||
)
|
||||
unstructured_result = _convert_to_content(result)
|
||||
|
||||
structured_output = None
|
||||
# First handle structured content based on output schema, if any
|
||||
|
|
@ -393,7 +388,6 @@ class TransformedTool(Tool):
|
|||
transform_args: dict[str, ArgTransform] | None = None,
|
||||
annotations: ToolAnnotations | NotSetT | None = NotSet,
|
||||
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
||||
serializer: Callable[[Any], str] | NotSetT | None = NotSet, # Deprecated
|
||||
meta: dict[str, Any] | NotSetT | None = NotSet,
|
||||
) -> TransformedTool:
|
||||
"""Create a transformed tool from a parent tool.
|
||||
|
|
@ -415,7 +409,6 @@ class TransformedTool(Tool):
|
|||
output_schema: Control output schema for structured outputs:
|
||||
- None (default): Inherit from transform_fn if available, then parent tool
|
||||
- dict: Use custom output schema
|
||||
serializer: Deprecated. Return ToolResult from your tools for full control over serialization.
|
||||
meta: Control meta information:
|
||||
- NotSet (default): Inherit from parent tool
|
||||
- dict: Use custom meta information
|
||||
|
|
@ -470,18 +463,6 @@ class TransformedTool(Tool):
|
|||
"""
|
||||
tool = Tool._ensure_tool(tool)
|
||||
|
||||
if (
|
||||
serializer is not NotSet
|
||||
and serializer is not None
|
||||
and fastmcp.settings.deprecation_warnings
|
||||
):
|
||||
warnings.warn(
|
||||
"The `serializer` parameter is deprecated. "
|
||||
"Return ToolResult from your tools for full control over serialization. "
|
||||
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
|
||||
FastMCPDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
transform_args = transform_args or {}
|
||||
|
||||
if transform_fn is not None:
|
||||
|
|
@ -601,9 +582,6 @@ class TransformedTool(Tool):
|
|||
final_annotations = (
|
||||
annotations if not isinstance(annotations, NotSetT) else tool.annotations
|
||||
)
|
||||
final_serializer = (
|
||||
serializer if not isinstance(serializer, NotSetT) else tool.serializer
|
||||
)
|
||||
|
||||
transformed_tool = cls(
|
||||
fn=final_fn,
|
||||
|
|
@ -617,7 +595,6 @@ class TransformedTool(Tool):
|
|||
output_schema=final_output_schema,
|
||||
tags=tags or tool.tags,
|
||||
annotations=final_annotations,
|
||||
serializer=final_serializer,
|
||||
meta=final_meta,
|
||||
transform_args=transform_args,
|
||||
auth=tool.auth,
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from mcp.server.session import ServerSession
|
||||
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.tools.base import Tool
|
||||
|
||||
|
||||
async def test_tool_exclude_args():
|
||||
"""Test that tool args are excluded."""
|
||||
mcp = FastMCP("Test Server")
|
||||
|
||||
@mcp.tool(exclude_args=["state"])
|
||||
def echo(message: str, state: dict[str, Any] | None = None) -> str:
|
||||
"""Echo back the message provided."""
|
||||
if state:
|
||||
# State was read
|
||||
pass
|
||||
return message
|
||||
|
||||
tools = await mcp.list_tools()
|
||||
assert len(tools) == 1
|
||||
assert "state" not in tools[0].parameters["properties"]
|
||||
|
||||
|
||||
async def test_tool_exclude_args_without_default_value_raises_error():
|
||||
"""Test that excluding args without default values raises ValueError"""
|
||||
mcp = FastMCP("Test Server")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
@mcp.tool(exclude_args=["state"])
|
||||
def echo(message: str, state: dict[str, Any] | None) -> str:
|
||||
"""Echo back the message provided."""
|
||||
if state:
|
||||
# State was read
|
||||
pass
|
||||
return message
|
||||
|
||||
|
||||
async def test_add_tool_method_exclude_args():
|
||||
"""Test that tool exclude_args work with the add_tool method."""
|
||||
mcp = FastMCP("Test Server")
|
||||
|
||||
def create_item(
|
||||
name: str, value: int, state: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Create a new item."""
|
||||
if state:
|
||||
# State was read
|
||||
pass
|
||||
return {"name": name, "value": value}
|
||||
|
||||
tool = Tool.from_function(
|
||||
create_item,
|
||||
name="create_item",
|
||||
exclude_args=["state"],
|
||||
)
|
||||
mcp.add_tool(tool)
|
||||
|
||||
# Check tool via public API
|
||||
tools = await mcp.list_tools()
|
||||
assert len(tools) == 1
|
||||
assert "state" not in tools[0].parameters["properties"]
|
||||
|
||||
|
||||
async def test_tool_functionality_with_exclude_args():
|
||||
"""Test that tool functionality is preserved when using exclude_args."""
|
||||
mcp = FastMCP("Test Server")
|
||||
|
||||
def create_item(
|
||||
name: str, value: int, state: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Create a new item."""
|
||||
if state:
|
||||
# state was read
|
||||
pass
|
||||
return {"name": name, "value": value}
|
||||
|
||||
tool = Tool.from_function(
|
||||
create_item,
|
||||
name="create_item",
|
||||
exclude_args=["state"],
|
||||
)
|
||||
mcp.add_tool(tool)
|
||||
|
||||
# Use the tool to verify functionality is preserved
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool(
|
||||
"create_item", {"name": "test_item", "value": 42}
|
||||
)
|
||||
assert result.data == {"name": "test_item", "value": 42}
|
||||
|
||||
|
||||
async def test_exclude_args_with_non_serializable_type():
|
||||
"""Test that exclude_args works even when the excluded parameter type can't be serialized.
|
||||
|
||||
This test ensures that exclude_args works correctly when the excluded parameter
|
||||
has a type that Pydantic cannot serialize (like ServerSession). The bug was that
|
||||
get_cached_typeadapter would try to serialize all parameters before compress_schema
|
||||
could exclude them, causing a PydanticSchemaGenerationError.
|
||||
"""
|
||||
|
||||
def my_tool(message: str, session: ServerSession | None = None) -> str:
|
||||
"""A tool that takes a non-serializable Session parameter."""
|
||||
return message
|
||||
|
||||
# This should not raise an error even though ServerSession can't be serialized
|
||||
tool = Tool.from_function(
|
||||
my_tool,
|
||||
name="my_tool",
|
||||
exclude_args=["session"],
|
||||
)
|
||||
|
||||
# Verify the tool was created successfully
|
||||
assert tool is not None
|
||||
assert tool.name == "my_tool"
|
||||
|
||||
# Verify the session parameter is excluded from the schema
|
||||
assert "session" not in tool.parameters["properties"]
|
||||
assert "message" in tool.parameters["properties"]
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
"""Test that deprecated import paths for function components still work."""
|
||||
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from fastmcp.utilities.tests import temporary_settings
|
||||
|
||||
|
||||
class TestDeprecatedFunctionToolImports:
|
||||
def test_function_tool_from_tool_module(self):
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(
|
||||
DeprecationWarning, match="Import from fastmcp.tools.function_tool"
|
||||
):
|
||||
from fastmcp.tools.base import FunctionTool
|
||||
|
||||
# Verify it's the real class
|
||||
from fastmcp.tools.function_tool import (
|
||||
FunctionTool as CanonicalFunctionTool,
|
||||
)
|
||||
|
||||
assert FunctionTool is CanonicalFunctionTool
|
||||
|
||||
def test_parsed_function_from_tool_module(self):
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(
|
||||
DeprecationWarning, match="Import from fastmcp.tools.function_tool"
|
||||
):
|
||||
from fastmcp.tools.base import ParsedFunction
|
||||
|
||||
from fastmcp.tools.function_tool import (
|
||||
ParsedFunction as CanonicalParsedFunction,
|
||||
)
|
||||
|
||||
assert ParsedFunction is CanonicalParsedFunction
|
||||
|
||||
def test_tool_decorator_from_tool_module(self):
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(
|
||||
DeprecationWarning, match="Import from fastmcp.tools.function_tool"
|
||||
):
|
||||
from fastmcp.tools.base import tool
|
||||
|
||||
from fastmcp.tools.function_tool import tool as canonical_tool
|
||||
|
||||
assert tool is canonical_tool
|
||||
|
||||
def test_no_warning_when_disabled(self):
|
||||
with temporary_settings(deprecation_warnings=False):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
from fastmcp.tools.base import FunctionTool # noqa: F401
|
||||
|
||||
|
||||
class TestDeprecatedFunctionResourceImports:
|
||||
def test_function_resource_from_resource_module(self):
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="Import from fastmcp.resources.function_resource",
|
||||
):
|
||||
from fastmcp.resources.base import FunctionResource
|
||||
|
||||
from fastmcp.resources.function_resource import (
|
||||
FunctionResource as CanonicalFunctionResource,
|
||||
)
|
||||
|
||||
assert FunctionResource is CanonicalFunctionResource
|
||||
|
||||
def test_resource_decorator_from_resource_module(self):
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="Import from fastmcp.resources.function_resource",
|
||||
):
|
||||
from fastmcp.resources.base import resource
|
||||
|
||||
from fastmcp.resources.function_resource import (
|
||||
resource as canonical_resource,
|
||||
)
|
||||
|
||||
assert resource is canonical_resource
|
||||
|
||||
def test_no_warning_when_disabled(self):
|
||||
with temporary_settings(deprecation_warnings=False):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
from fastmcp.resources.base import FunctionResource # noqa: F401
|
||||
|
||||
|
||||
class TestDeprecatedFunctionPromptImports:
|
||||
def test_function_prompt_from_prompt_module(self):
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(
|
||||
DeprecationWarning, match="Import from fastmcp.prompts.function_prompt"
|
||||
):
|
||||
from fastmcp.prompts.base import FunctionPrompt
|
||||
|
||||
from fastmcp.prompts.function_prompt import (
|
||||
FunctionPrompt as CanonicalFunctionPrompt,
|
||||
)
|
||||
|
||||
assert FunctionPrompt is CanonicalFunctionPrompt
|
||||
|
||||
def test_prompt_decorator_from_prompt_module(self):
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(
|
||||
DeprecationWarning, match="Import from fastmcp.prompts.function_prompt"
|
||||
):
|
||||
from fastmcp.prompts.base import prompt
|
||||
|
||||
from fastmcp.prompts.function_prompt import prompt as canonical_prompt
|
||||
|
||||
assert prompt is canonical_prompt
|
||||
|
||||
def test_no_warning_when_disabled(self):
|
||||
with temporary_settings(deprecation_warnings=False):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
from fastmcp.prompts.base import FunctionPrompt # noqa: F401
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
"""Tests for deprecated tool serializer functionality.
|
||||
|
||||
These tests verify that serializer parameters still work but are deprecated.
|
||||
All serializer-related tests should be moved here.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp_types import TextContent
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.contrib.mcp_mixin import mcp_tool
|
||||
from fastmcp.server.providers import LocalProvider
|
||||
from fastmcp.tools.base import Tool, _convert_to_content
|
||||
from fastmcp.tools.tool_transform import TransformedTool
|
||||
from fastmcp.utilities.tests import temporary_settings
|
||||
|
||||
|
||||
class TestToolSerializerDeprecated:
|
||||
"""Tests for deprecated serializer functionality."""
|
||||
|
||||
async def test_tool_serializer(self):
|
||||
"""Test that a tool's serializer is used to serialize the result."""
|
||||
|
||||
def custom_serializer(data) -> str:
|
||||
return f"Custom serializer: {data}"
|
||||
|
||||
def process_list(items: list[int]) -> int:
|
||||
return sum(items)
|
||||
|
||||
tool = Tool.from_function(process_list, serializer=custom_serializer)
|
||||
|
||||
result = await tool.run(arguments={"items": [1, 2, 3, 4, 5]})
|
||||
# Custom serializer affects unstructured content
|
||||
assert isinstance(result.content[0], TextContent)
|
||||
assert result.content[0].text == "Custom serializer: 15"
|
||||
# Structured output should have the raw value
|
||||
assert result.structured_content == {"result": 15}
|
||||
|
||||
def test_custom_serializer(self):
|
||||
"""Test that a custom serializer is used for non-MCP types."""
|
||||
|
||||
def custom_serializer(data):
|
||||
return f"Serialized: {data}"
|
||||
|
||||
result = _convert_to_content({"a": 1}, serializer=custom_serializer)
|
||||
|
||||
assert result == snapshot(
|
||||
[TextContent(type="text", text="Serialized: {'a': 1}")]
|
||||
)
|
||||
|
||||
def test_custom_serializer_error_fallback(self, caplog):
|
||||
"""Test that if a custom serializer fails, it falls back to the default."""
|
||||
|
||||
def custom_serializer_that_fails(data):
|
||||
raise ValueError("Serialization failed")
|
||||
|
||||
result = _convert_to_content({"a": 1}, serializer=custom_serializer_that_fails)
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert result == snapshot([TextContent(type="text", text='{"a":1}')])
|
||||
|
||||
assert "Error serializing tool result" in caplog.text
|
||||
|
||||
|
||||
class TestSerializerDeprecationWarnings:
|
||||
"""Tests that deprecation warnings are raised when serializer is used."""
|
||||
|
||||
def test_tool_from_function_serializer_warning(self):
|
||||
"""Test that Tool.from_function warns when serializer is provided."""
|
||||
|
||||
def custom_serializer(data) -> str:
|
||||
return f"Custom: {data}"
|
||||
|
||||
def my_tool(x: int) -> int:
|
||||
return x * 2
|
||||
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
|
||||
Tool.from_function(my_tool, serializer=custom_serializer)
|
||||
|
||||
def test_tool_from_function_serializer_no_warning_when_disabled(self):
|
||||
"""Test that no warning is raised when deprecation_warnings is False."""
|
||||
|
||||
def custom_serializer(data) -> str:
|
||||
return f"Custom: {data}"
|
||||
|
||||
def my_tool(x: int) -> int:
|
||||
return x * 2
|
||||
|
||||
with temporary_settings(deprecation_warnings=False):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
# Should not raise
|
||||
Tool.from_function(my_tool, serializer=custom_serializer)
|
||||
|
||||
def test_local_provider_tool_serializer_warning(self):
|
||||
"""Test that LocalProvider.tool warns when serializer is provided."""
|
||||
provider = LocalProvider()
|
||||
|
||||
def custom_serializer(data) -> str:
|
||||
return f"Custom: {data}"
|
||||
|
||||
def my_tool(x: int) -> int:
|
||||
return x * 2
|
||||
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
|
||||
provider.tool(my_tool, serializer=custom_serializer)
|
||||
|
||||
def test_local_provider_tool_decorator_serializer_warning(self):
|
||||
"""Test that LocalProvider.tool decorator warns when serializer is provided."""
|
||||
provider = LocalProvider()
|
||||
|
||||
def custom_serializer(data) -> str:
|
||||
return f"Custom: {data}"
|
||||
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
|
||||
|
||||
@provider.tool(serializer=custom_serializer)
|
||||
def my_tool(x: int) -> int:
|
||||
return x * 2
|
||||
|
||||
def test_fastmcp_tool_serializer_warning(self):
|
||||
"""Test that FastMCP.tool warns when serializer is provided via LocalProvider."""
|
||||
|
||||
def custom_serializer(data) -> str:
|
||||
return f"Custom: {data}"
|
||||
|
||||
def my_tool(x: int) -> int:
|
||||
return x * 2
|
||||
|
||||
# FastMCP.tool doesn't accept serializer directly, it goes through LocalProvider
|
||||
# So we test LocalProvider.tool which is what FastMCP uses internally
|
||||
provider = LocalProvider()
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
|
||||
provider.tool(my_tool, serializer=custom_serializer)
|
||||
|
||||
def test_fastmcp_tool_serializer_parameter_raises_type_error(self):
|
||||
"""Test that FastMCP tool_serializer parameter raises TypeError."""
|
||||
|
||||
def custom_serializer(data) -> str:
|
||||
return f"Custom: {data}"
|
||||
|
||||
with pytest.raises(TypeError, match="no longer accepts `tool_serializer`"):
|
||||
FastMCP("TestServer", tool_serializer=custom_serializer)
|
||||
|
||||
def test_transformed_tool_from_tool_serializer_warning(self):
|
||||
"""Test that TransformedTool.from_tool warns when serializer is provided."""
|
||||
|
||||
def custom_serializer(data) -> str:
|
||||
return f"Custom: {data}"
|
||||
|
||||
def my_tool(x: int) -> int:
|
||||
return x * 2
|
||||
|
||||
parent_tool = Tool.from_function(my_tool)
|
||||
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
|
||||
TransformedTool.from_tool(parent_tool, serializer=custom_serializer)
|
||||
|
||||
def test_mcp_mixin_tool_serializer_warning(self):
|
||||
"""Test that mcp_tool decorator warns when serializer is provided."""
|
||||
|
||||
def custom_serializer(data) -> str:
|
||||
return f"Custom: {data}"
|
||||
|
||||
with temporary_settings(deprecation_warnings=True):
|
||||
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
|
||||
|
||||
@mcp_tool(serializer=custom_serializer)
|
||||
def my_tool(x: int) -> int:
|
||||
return x * 2
|
||||
Loading…
Add table
Add a link
Reference in a new issue