From 25e2f4da32bd2ceb4c1d866d3dedc4a0906df4be Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:39:34 -0500 Subject: [PATCH] Remove deprecated FastMCP() constructor kwargs (#3148) Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> --- docs/development/v3-notes/v3-features.mdx | 56 +-- docs/python-sdk/fastmcp-mcp_config.mdx | 32 +- .../fastmcp-server-mixins-transport.mdx | 2 +- docs/python-sdk/fastmcp-server-server.mdx | 108 +++--- docs/python-sdk/fastmcp-settings.mdx | 12 +- src/fastmcp/mcp_config.py | 7 +- src/fastmcp/server/mixins/transport.py | 25 +- src/fastmcp/server/server.py | 200 ++-------- src/fastmcp/settings.py | 27 -- .../server/test_include_exclude_tags.py | 69 +--- .../test_add_tool_transformation.py | 35 +- tests/deprecated/test_deprecated.py | 49 +-- tests/deprecated/test_openapi_deprecations.py | 25 -- tests/deprecated/test_settings.py | 361 +++--------------- tests/deprecated/test_tool_serializer.py | 9 +- tests/server/mount/test_filtering.py | 12 +- .../local_provider_tools/test_tags.py | 7 +- .../providers/test_local_provider_prompts.py | 7 +- .../test_local_provider_resources.py | 14 +- tests/server/test_server.py | 4 +- tests/utilities/test_inspect.py | 12 +- 21 files changed, 279 insertions(+), 794 deletions(-) diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx index 690390e39..93420c6b8 100644 --- a/docs/development/v3-notes/v3-features.mdx +++ b/docs/development/v3-notes/v3-features.mdx @@ -66,6 +66,32 @@ async def get_emails( Background tasks now use a distributed Redis notification queue for reliable delivery ([#2906](https://github.com/jlowin/fastmcp/pull/2906)). Elicitation switches from polling to BLPOP (single blocking call instead of ~7,200 round-trips/hour), and notification delivery retries up to 3x with TTL-based expiration. +### Breaking: Deprecated `FastMCP()` Constructor Kwargs Removed + +Sixteen deprecated keyword arguments have been removed from `FastMCP.__init__`. Passing any of them now raises `TypeError` with a migration hint. Environment variables (e.g., `FASTMCP_HOST`) continue to work — only the constructor kwargs moved. + +**Transport/server settings** (`host`, `port`, `log_level`, `debug`, `sse_path`, `message_path`, `streamable_http_path`, `json_response`, `stateless_http`): Pass to `run()`, `run_http_async()`, or `http_app()` as appropriate, or set via environment variables. + +```python +# Before +mcp = FastMCP("server", host="0.0.0.0", port=8080) +mcp.run() + +# After +mcp = FastMCP("server") +mcp.run(transport="http", host="0.0.0.0", port=8080) +``` + +**Duplicate handling** (`on_duplicate_tools`, `on_duplicate_resources`, `on_duplicate_prompts`): Use the unified `on_duplicate=` parameter. + +**Tag filtering** (`include_tags`, `exclude_tags`): Use `server.enable(tags=..., only=True)` and `server.disable(tags=...)` after construction. + +**Tool serializer** (`tool_serializer`): Return `ToolResult` from tools instead. + +**Tool transformations** (`tool_transformations`): Use `server.add_transform(ToolTransform(...))` after construction. + +The `_deprecated_settings` attribute and `.settings` property are also removed. `ExperimentalSettings` has been deleted (dead code). + ### Breaking: `ui=` Renamed to `app=` The MCP Apps decorator parameter has been renamed from `ui=ToolUI(...)` / `ui=ResourceUI(...)` to `app=AppConfig(...)` ([#3117](https://github.com/jlowin/fastmcp/pull/3117)). `ToolUI` and `ResourceUI` are consolidated into a single `AppConfig` class. Wire format is unchanged. See the MCP Apps section under beta2 for full details. @@ -1266,35 +1292,9 @@ main.mount(subserver, prefix="api") main.mount(subserver, namespace="api") ``` -#### Tag Filtering Init Parameters +#### Tag Filtering, Tool Serializer, Tool Transformations Init Parameters -`FastMCP(include_tags=..., exclude_tags=...)` deprecated. Use `enable()`/`disable()` methods: - -```python -# Deprecated -mcp = FastMCP("server", exclude_tags={"internal"}) - -# New -mcp = FastMCP("server") -mcp.disable(tags={"internal"}) -``` - -#### Tool Serializer Parameter - -The `tool_serializer` parameter on `FastMCP` is deprecated. Return `ToolResult` for explicit serialization control. - -#### Tool Transformation Methods - -`add_tool_transformation()`, `remove_tool_transformation()`, and `tool_transformations` constructor parameter are deprecated. Use `add_transform(ToolTransform({...}))` instead: - -```python -# Deprecated -mcp.add_tool_transformation("name", config) - -# New -from fastmcp.server.transforms import ToolTransform -mcp.add_transform(ToolTransform({"name": config})) -``` +These constructor parameters have been **removed** (not just deprecated) as of rc1. See "Breaking: Deprecated `FastMCP()` Constructor Kwargs Removed" in the rc1 section above. The `add_tool_transformation()` and `remove_tool_transformation()` methods remain as deprecated shims. --- diff --git a/docs/python-sdk/fastmcp-mcp_config.mdx b/docs/python-sdk/fastmcp-mcp_config.mdx index 87ed568a0..71046dd4a 100644 --- a/docs/python-sdk/fastmcp-mcp_config.mdx +++ b/docs/python-sdk/fastmcp-mcp_config.mdx @@ -42,7 +42,7 @@ infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse'] Infer the appropriate transport type from the given URL. -### `update_config_file` +### `update_config_file` ```python update_config_file(file_path: Path, server_name: str, server_config: CanonicalMCPServerTypes) -> None @@ -57,7 +57,7 @@ worry about transforming server objects here. ## Classes -### `StdioMCPServer` +### `StdioMCPServer` MCP server configuration for stdio transport. @@ -67,19 +67,19 @@ This is the canonical configuration format for MCP servers using stdio transport **Methods:** -#### `to_transport` +#### `to_transport` ```python to_transport(self) -> StdioTransport ``` -### `TransformingStdioMCPServer` +### `TransformingStdioMCPServer` A Stdio server with tool transforms. -### `RemoteMCPServer` +### `RemoteMCPServer` MCP server configuration for HTTP/SSE transport. @@ -89,19 +89,19 @@ This is the canonical configuration format for MCP servers using remote transpor **Methods:** -#### `to_transport` +#### `to_transport` ```python to_transport(self) -> StreamableHttpTransport | SSETransport ``` -### `TransformingRemoteMCPServer` +### `TransformingRemoteMCPServer` A Remote server with tool transforms. -### `MCPConfig` +### `MCPConfig` A configuration object for MCP Servers that conforms to the canonical MCP configuration format @@ -113,7 +113,7 @@ For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class. **Methods:** -#### `wrap_servers_at_root` +#### `wrap_servers_at_root` ```python wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any] @@ -122,7 +122,7 @@ wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any] If there's no mcpServers key but there are server configs at root, wrap them. -#### `add_server` +#### `add_server` ```python add_server(self, name: str, server: MCPServerTypes) -> None @@ -131,7 +131,7 @@ add_server(self, name: str, server: MCPServerTypes) -> None Add or update a server in the configuration. -#### `from_dict` +#### `from_dict` ```python from_dict(cls, config: dict[str, Any]) -> Self @@ -140,7 +140,7 @@ from_dict(cls, config: dict[str, Any]) -> Self Parse MCP configuration from dictionary format. -#### `to_dict` +#### `to_dict` ```python to_dict(self) -> dict[str, Any] @@ -149,7 +149,7 @@ to_dict(self) -> dict[str, Any] Convert MCPConfig to dictionary format, preserving all fields. -#### `write_to_file` +#### `write_to_file` ```python write_to_file(self, file_path: Path) -> None @@ -158,7 +158,7 @@ write_to_file(self, file_path: Path) -> None Write configuration to JSON file. -#### `from_file` +#### `from_file` ```python from_file(cls, file_path: Path) -> Self @@ -167,7 +167,7 @@ from_file(cls, file_path: Path) -> Self Load configuration from JSON file. -### `CanonicalMCPConfig` +### `CanonicalMCPConfig` Canonical MCP configuration format. @@ -178,7 +178,7 @@ The format is designed to be client-agnostic and extensible for future use cases **Methods:** -#### `add_server` +#### `add_server` ```python add_server(self, name: str, server: CanonicalMCPServerTypes) -> None diff --git a/docs/python-sdk/fastmcp-server-mixins-transport.mdx b/docs/python-sdk/fastmcp-server-mixins-transport.mdx index ed61e5a5d..0ce07f2fb 100644 --- a/docs/python-sdk/fastmcp-server-mixins-transport.mdx +++ b/docs/python-sdk/fastmcp-server-mixins-transport.mdx @@ -104,7 +104,7 @@ Run the server using HTTP transport. - `stateless`: Alias for stateless_http for CLI consistency -#### `http_app` +#### `http_app` ```python http_app(self: FastMCP, path: str | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None, transport: Literal['http', 'streamable-http', 'sse'] = 'http', event_store: EventStore | None = None, retry_interval: int | None = None) -> StarletteWithLifespan diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx index 80b45257b..efaa20d12 100644 --- a/docs/python-sdk/fastmcp-server-server.mdx +++ b/docs/python-sdk/fastmcp-server-server.mdx @@ -10,7 +10,7 @@ FastMCP - A more ergonomic interface for MCP servers. ## Functions -### `default_lifespan` +### `default_lifespan` ```python default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any] @@ -26,7 +26,7 @@ Default lifespan context manager that does nothing. - An empty dictionary as the lifespan result. -### `create_proxy` +### `create_proxy` ```python create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy @@ -54,65 +54,59 @@ use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.pr ## Classes -### `StateValue` +### `StateValue` Wrapper for stored context state values. -### `FastMCP` +### `FastMCP` **Methods:** -#### `settings` - -```python -settings(self) -> Settings -``` - -#### `name` +#### `name` ```python name(self) -> str ``` -#### `instructions` +#### `instructions` ```python instructions(self) -> str | None ``` -#### `instructions` +#### `instructions` ```python instructions(self, value: str | None) -> None ``` -#### `version` +#### `version` ```python version(self) -> str | None ``` -#### `website_url` +#### `website_url` ```python website_url(self) -> str | None ``` -#### `icons` +#### `icons` ```python icons(self) -> list[mcp.types.Icon] ``` -#### `add_middleware` +#### `add_middleware` ```python add_middleware(self, middleware: Middleware) -> None ``` -#### `add_provider` +#### `add_provider` ```python add_provider(self, provider: Provider) -> None @@ -132,7 +126,7 @@ always take precedence over providers. - Prompts become "namespace_promptname" -#### `get_tasks` +#### `get_tasks` ```python get_tasks(self) -> Sequence[FastMCPComponent] @@ -144,7 +138,7 @@ Overrides AggregateProvider.get_tasks() to apply server-level transforms after aggregation. AggregateProvider handles provider-level namespacing. -#### `add_transform` +#### `add_transform` ```python add_transform(self, transform: Transform) -> None @@ -159,7 +153,7 @@ They transform tools, resources, and prompts from ALL providers. - `transform`: The transform to add. -#### `add_tool_transformation` +#### `add_tool_transformation` ```python add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None @@ -171,7 +165,7 @@ Add a tool transformation. Use ``add_transform(ToolTransform({...}))`` instead. -#### `remove_tool_transformation` +#### `remove_tool_transformation` ```python remove_tool_transformation(self, _tool_name: str) -> None @@ -183,7 +177,7 @@ Remove a tool transformation. Tool transformations are now immutable. Use enable/disable controls instead. -#### `list_tools` +#### `list_tools` ```python list_tools(self) -> Sequence[Tool] @@ -196,7 +190,7 @@ and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. -#### `get_tool` +#### `get_tool` ```python get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None @@ -216,7 +210,7 @@ session transforms can override provider-level disables. - The tool if found and enabled, None otherwise. -#### `list_resources` +#### `list_resources` ```python list_resources(self) -> Sequence[Resource] @@ -229,7 +223,7 @@ and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. -#### `get_resource` +#### `get_resource` ```python get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None @@ -248,7 +242,7 @@ transforms (including session-level) have been applied. - The resource if found and enabled, None otherwise. -#### `list_resource_templates` +#### `list_resource_templates` ```python list_resource_templates(self) -> Sequence[ResourceTemplate] @@ -261,7 +255,7 @@ auth filtering, and middleware execution. Returns all versions (no deduplication Protocol handlers deduplicate for MCP wire format. -#### `get_resource_template` +#### `get_resource_template` ```python get_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None @@ -280,7 +274,7 @@ all transforms (including session-level) have been applied. - The template if found and enabled, None otherwise. -#### `list_prompts` +#### `list_prompts` ```python list_prompts(self) -> Sequence[Prompt] @@ -293,7 +287,7 @@ and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. -#### `get_prompt` +#### `get_prompt` ```python get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None @@ -312,19 +306,19 @@ transforms (including session-level) have been applied. - The prompt if found and enabled, None otherwise. -#### `call_tool` +#### `call_tool` ```python call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult ``` -#### `call_tool` +#### `call_tool` ```python call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult ``` -#### `call_tool` +#### `call_tool` ```python call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult | mcp.types.CreateTaskResult @@ -354,19 +348,19 @@ return ToolResult. - `ValidationError`: If arguments fail validation -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: str) -> ResourceResult ``` -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: str) -> mcp.types.CreateTaskResult ``` -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: str) -> ResourceResult | mcp.types.CreateTaskResult @@ -395,19 +389,19 @@ return ResourceResult. - `ResourceError`: If resource read fails -#### `render_prompt` +#### `render_prompt` ```python render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult ``` -#### `render_prompt` +#### `render_prompt` ```python render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult ``` -#### `render_prompt` +#### `render_prompt` ```python render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult | mcp.types.CreateTaskResult @@ -437,7 +431,7 @@ return PromptResult. - `PromptError`: If prompt rendering fails -#### `add_tool` +#### `add_tool` ```python add_tool(self, tool: Tool | Callable[..., Any]) -> Tool @@ -455,7 +449,7 @@ with the Context type annotation. See the @tool decorator for examples. - The tool instance that was added to the server. -#### `remove_tool` +#### `remove_tool` ```python remove_tool(self, name: str, version: str | None = None) -> None @@ -471,19 +465,19 @@ Remove tool(s) from the server. - `NotFoundError`: If no matching tool is found. -#### `tool` +#### `tool` ```python tool(self, name_or_fn: AnyFunction) -> FunctionTool ``` -#### `tool` +#### `tool` ```python tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool] ``` -#### `tool` +#### `tool` ```python tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool] @@ -539,7 +533,7 @@ server.tool(my_function, name="custom_name") ``` -#### `add_resource` +#### `add_resource` ```python add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate @@ -554,7 +548,7 @@ Add a resource to the server. - The resource instance that was added to the server. -#### `add_template` +#### `add_template` ```python add_template(self, template: ResourceTemplate) -> ResourceTemplate @@ -569,7 +563,7 @@ Add a resource template to the server. - The template instance that was added to the server. -#### `resource` +#### `resource` ```python resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction] @@ -628,7 +622,7 @@ async def get_weather(city: str) -> str: ``` -#### `add_prompt` +#### `add_prompt` ```python add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt @@ -643,19 +637,19 @@ Add a prompt to the server. - The prompt instance that was added to the server. -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt ``` -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt] ``` -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt] @@ -732,7 +726,7 @@ Decorator to register a prompt. ``` -#### `mount` +#### `mount` ```python mount(self, server: FastMCP[LifespanResultT], namespace: str | None = None, as_proxy: bool | None = None, tool_names: dict[str, str] | None = None, prefix: str | None = None) -> None @@ -779,7 +773,7 @@ mounted server. - `prefix`: Deprecated. Use namespace instead. -#### `import_server` +#### `import_server` ```python import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None) -> None @@ -820,7 +814,7 @@ templates, and prompts are imported with their original names. objects are imported with their original names. -#### `from_openapi` +#### `from_openapi` ```python from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, validate_output: bool = True, **settings: Any) -> Self @@ -849,7 +843,7 @@ response structure while still returning structured JSON. - A FastMCP server with an OpenAPIProvider attached. -#### `from_fastapi` +#### `from_fastapi` ```python from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> Self @@ -873,7 +867,7 @@ Use this to configure timeout and other client settings. - A FastMCP server with an OpenAPIProvider attached. -#### `as_proxy` +#### `as_proxy` ```python as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy @@ -891,7 +885,7 @@ instance or any value accepted as the `transport` argument of `fastmcp.client.Client` constructor. -#### `generate_name` +#### `generate_name` ```python generate_name(cls, name: str | None = None) -> str diff --git a/docs/python-sdk/fastmcp-settings.mdx b/docs/python-sdk/fastmcp-settings.mdx index 8c6470a39..6b630615a 100644 --- a/docs/python-sdk/fastmcp-settings.mdx +++ b/docs/python-sdk/fastmcp-settings.mdx @@ -7,15 +7,13 @@ sidebarTitle: settings ## Classes -### `DocketSettings` +### `DocketSettings` Docket worker configuration. -### `ExperimentalSettings` - -### `Settings` +### `Settings` FastMCP settings. @@ -23,7 +21,7 @@ FastMCP settings. **Methods:** -#### `get_setting` +#### `get_setting` ```python get_setting(self, attr: str) -> Any @@ -33,7 +31,7 @@ Get a setting. If the setting contains one or more `__`, it will be treated as a nested setting. -#### `set_setting` +#### `set_setting` ```python set_setting(self, attr: str, value: Any) -> None @@ -43,7 +41,7 @@ Set a setting. If the setting contains one or more `__`, it will be treated as a nested setting. -#### `normalize_log_level` +#### `normalize_log_level` ```python normalize_log_level(cls, v) diff --git a/src/fastmcp/mcp_config.py b/src/fastmcp/mcp_config.py index f65132edb..c7dac539f 100644 --- a/src/fastmcp/mcp_config.py +++ b/src/fastmcp/mcp_config.py @@ -109,10 +109,13 @@ class _TransformingMCPServerMixin(FastMCPBaseModel): wrapped_mcp_server = create_proxy( client, name=server_name, - include_tags=self.include_tags, - exclude_tags=self.exclude_tags, ) + if self.include_tags is not None: + wrapped_mcp_server.enable(tags=self.include_tags, only=True) + if self.exclude_tags is not None: + wrapped_mcp_server.disable(tags=self.exclude_tags) + # Apply tool transforms if configured if self.tools: from fastmcp.server.transforms import ToolTransform diff --git a/src/fastmcp/server/mixins/transport.py b/src/fastmcp/server/mixins/transport.py index 9e797dc1f..1f069a02d 100644 --- a/src/fastmcp/server/mixins/transport.py +++ b/src/fastmcp/server/mixins/transport.py @@ -231,17 +231,15 @@ class TransportMixin: # Resolve from settings/env var if not explicitly set if stateless_http is None: - stateless_http = self._deprecated_settings.stateless_http + stateless_http = fastmcp.settings.stateless_http # SSE doesn't support stateless mode if stateless_http and transport == "sse": raise ValueError("SSE transport does not support stateless mode") - host = host or self._deprecated_settings.host - port = port or self._deprecated_settings.port - default_log_level_to_use = ( - log_level or self._deprecated_settings.log_level - ).lower() + host = host or fastmcp.settings.host + port = port or fastmcp.settings.port + default_log_level_to_use = (log_level or fastmcp.settings.log_level).lower() app = self.http_app( path=path, @@ -311,31 +309,30 @@ class TransportMixin: if transport in ("streamable-http", "http"): return create_streamable_http_app( server=self, - streamable_http_path=path - or self._deprecated_settings.streamable_http_path, + streamable_http_path=path or fastmcp.settings.streamable_http_path, event_store=event_store, retry_interval=retry_interval, auth=self.auth, json_response=( json_response if json_response is not None - else self._deprecated_settings.json_response + else fastmcp.settings.json_response ), stateless_http=( stateless_http if stateless_http is not None - else self._deprecated_settings.stateless_http + else fastmcp.settings.stateless_http ), - debug=self._deprecated_settings.debug, + debug=fastmcp.settings.debug, middleware=middleware, ) elif transport == "sse": return create_sse_app( server=self, - message_path=self._deprecated_settings.message_path, - sse_path=path or self._deprecated_settings.sse_path, + message_path=fastmcp.settings.message_path, + sse_path=path or fastmcp.settings.sse_path, auth=self.auth, - debug=self._deprecated_settings.debug, + debug=fastmcp.settings.debug, middleware=middleware, ) else: diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 35d717e8c..66c60d978 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -10,8 +10,6 @@ from collections.abc import ( AsyncIterator, Awaitable, Callable, - Collection, - Mapping, Sequence, ) from contextlib import ( @@ -79,7 +77,6 @@ from fastmcp.server.transforms import ( ) from fastmcp.server.transforms.visibility import apply_session_transforms, is_enabled from fastmcp.settings import DuplicateBehavior as DuplicateBehaviorSetting -from fastmcp.settings import Settings from fastmcp.tools.function_tool import FunctionTool from fastmcp.tools.tool import AuthCheckCallable, Tool, ToolResult from fastmcp.tools.tool_transform import ToolTransformConfig @@ -99,7 +96,6 @@ if TYPE_CHECKING: from fastmcp.server.providers.openapi import RouteMap from fastmcp.server.providers.openapi import RouteMapFn as OpenAPIRouteMapFn from fastmcp.server.providers.proxy import FastMCPProxy - from fastmcp.tools.tool import ToolResultSerializerType logger = get_logger(__name__) @@ -107,39 +103,37 @@ logger = get_logger(__name__) DuplicateBehavior = Literal["warn", "error", "replace", "ignore"] -def _resolve_on_duplicate( - on_duplicate: DuplicateBehavior | None, - on_duplicate_tools: DuplicateBehavior | None, - on_duplicate_resources: DuplicateBehavior | None, - on_duplicate_prompts: DuplicateBehavior | None, -) -> DuplicateBehavior: - """Resolve on_duplicate from deprecated per-type params. +_REMOVED_KWARGS: dict[str, str] = { + "host": "Pass `host` to `run_http_async()`, or set FASTMCP_HOST.", + "port": "Pass `port` to `run_http_async()`, or set FASTMCP_PORT.", + "sse_path": "Pass `path` to `run_http_async()` or `http_app()`, or set FASTMCP_SSE_PATH.", + "message_path": "Set FASTMCP_MESSAGE_PATH.", + "streamable_http_path": "Pass `path` to `run_http_async()` or `http_app()`, or set FASTMCP_STREAMABLE_HTTP_PATH.", + "json_response": "Pass `json_response` to `run_http_async()` or `http_app()`, or set FASTMCP_JSON_RESPONSE.", + "stateless_http": "Pass `stateless_http` to `run_http_async()` or `http_app()`, or set FASTMCP_STATELESS_HTTP.", + "debug": "Set FASTMCP_DEBUG.", + "log_level": "Pass `log_level` to `run_http_async()`, or set FASTMCP_LOG_LEVEL.", + "on_duplicate_tools": "Use `on_duplicate=` instead.", + "on_duplicate_resources": "Use `on_duplicate=` instead.", + "on_duplicate_prompts": "Use `on_duplicate=` instead.", + "tool_serializer": "Return ToolResult from your tools instead. See https://gofastmcp.com/servers/tools#custom-serialization", + "include_tags": "Use `server.enable(tags=..., only=True)` after creating the server.", + "exclude_tags": "Use `server.disable(tags=...)` after creating the server.", + "tool_transformations": "Use `server.add_transform(ToolTransform(...))` after creating the server.", +} - Takes the most strict value if multiple are provided. - Delete this function when removing deprecated params. - """ - strictness_order: list[DuplicateBehavior] = ["error", "warn", "replace", "ignore"] - deprecated_values: list[DuplicateBehavior] = [] - deprecated_params: list[tuple[str, DuplicateBehavior | None]] = [ - ("on_duplicate_tools", on_duplicate_tools), - ("on_duplicate_resources", on_duplicate_resources), - ("on_duplicate_prompts", on_duplicate_prompts), - ] - for name, value in deprecated_params: - if value is not None: - if fastmcp.settings.deprecation_warnings: - warnings.warn( - f"{name} is deprecated, use on_duplicate instead", - DeprecationWarning, - stacklevel=4, - ) - deprecated_values.append(value) - - if on_duplicate is None and deprecated_values: - return min(deprecated_values, key=lambda x: strictness_order.index(x)) - - return on_duplicate or "warn" +def _check_removed_kwargs(kwargs: dict[str, Any]) -> None: + """Raise helpful TypeErrors for kwargs removed in v3.""" + for key in kwargs: + if key in _REMOVED_KWARGS: + raise TypeError( + f"FastMCP() no longer accepts `{key}`. {_REMOVED_KWARGS[key]}" + ) + if kwargs: + raise TypeError( + f"FastMCP() got unexpected keyword argument(s): {', '.join(repr(k) for k in kwargs)}" + ) Transport = Literal["stdio", "http", "sse", "streamable-http"] @@ -232,45 +226,23 @@ class FastMCP( middleware: Sequence[Middleware] | None = None, providers: Sequence[Provider] | None = None, lifespan: LifespanCallable | Lifespan | None = None, - mask_error_details: bool | None = None, tools: Sequence[Tool | Callable[..., Any]] | None = None, - tool_serializer: ToolResultSerializerType | None = None, - include_tags: Collection[str] | None = None, - exclude_tags: Collection[str] | None = None, on_duplicate: DuplicateBehavior | None = None, + mask_error_details: bool | None = None, strict_input_validation: bool | None = None, list_page_size: int | None = None, tasks: bool | None = None, session_state_store: AsyncKeyValue | None = None, - # --- - # --- DEPRECATED parameters --- - # --- - on_duplicate_tools: DuplicateBehavior | None = None, - on_duplicate_resources: DuplicateBehavior | None = None, - on_duplicate_prompts: DuplicateBehavior | None = None, - log_level: str | None = None, - debug: bool | None = None, - host: str | None = None, - port: int | None = None, - sse_path: str | None = None, - message_path: str | None = None, - streamable_http_path: str | None = None, - json_response: bool | None = None, - stateless_http: bool | None = None, sampling_handler: SamplingHandler | None = None, sampling_handler_behavior: Literal["always", "fallback"] | None = None, - tool_transformations: Mapping[str, ToolTransformConfig] | None = None, + **kwargs: Any, ): + _check_removed_kwargs(kwargs) + # Initialize Provider (sets up _transforms) super().__init__() - # Resolve on_duplicate from deprecated params (delete when removing deprecation) - self._on_duplicate: DuplicateBehaviorSetting = _resolve_on_duplicate( - on_duplicate, - on_duplicate_tools, - on_duplicate_resources, - on_duplicate_prompts, - ) + self._on_duplicate: DuplicateBehaviorSetting = on_duplicate or "warn" # Resolve server default for background task support self._support_tasks_by_default: bool = tasks if tasks is not None else False @@ -312,16 +284,6 @@ class FastMCP( raise ValueError("list_page_size must be a positive integer") self._list_page_size: int | None = list_page_size - if tool_serializer is not None and fastmcp.settings.deprecation_warnings: - warnings.warn( - "The `tool_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.", - DeprecationWarning, - stacklevel=2, - ) - self._tool_serializer: Callable[[Any], str] | None = tool_serializer - # Handle Lifespan instances (they're callable) or regular lifespan functions if lifespan is not None: self._lifespan: LifespanCallable[LifespanResultT] = lifespan @@ -349,38 +311,9 @@ class FastMCP( if tools: for tool in tools: if not isinstance(tool, Tool): - tool = Tool.from_function(tool, serializer=self._tool_serializer) + tool = Tool.from_function(tool) self.add_tool(tool) - # Handle deprecated include_tags and exclude_tags parameters - if include_tags is not None: - warnings.warn( - "include_tags is deprecated. Use server.enable(tags=..., only=True) instead.", - DeprecationWarning, - stacklevel=2, - ) - # For backwards compatibility, initialize allowlist from include_tags - self.enable(tags=set(include_tags), only=True) - if exclude_tags is not None: - warnings.warn( - "exclude_tags is deprecated. Use server.disable(tags=...) instead.", - DeprecationWarning, - stacklevel=2, - ) - # For backwards compatibility, initialize blocklist from exclude_tags - self.disable(tags=set(exclude_tags)) - - # Handle deprecated tool_transformations parameter - if tool_transformations: - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "The tool_transformations parameter is deprecated. Use " - "server.add_transform(ToolTransform({...})) instead.", - DeprecationWarning, - stacklevel=2, - ) - self._transforms.append(ToolTransform(dict(tool_transformations))) - self.strict_input_validation: bool = ( strict_input_validation if strict_input_validation is not None @@ -397,71 +330,9 @@ class FastMCP( sampling_handler_behavior or "fallback" ) - self._handle_deprecated_settings( - log_level=log_level, - debug=debug, - host=host, - port=port, - sse_path=sse_path, - message_path=message_path, - streamable_http_path=streamable_http_path, - json_response=json_response, - stateless_http=stateless_http, - ) - def __repr__(self) -> str: return f"{type(self).__name__}({self.name!r})" - def _handle_deprecated_settings( - self, - log_level: str | None, - debug: bool | None, - host: str | None, - port: int | None, - sse_path: str | None, - message_path: str | None, - streamable_http_path: str | None, - json_response: bool | None, - stateless_http: bool | None, - ) -> None: - """Handle deprecated settings. Deprecated in 2.8.0.""" - deprecated_settings: dict[str, Any] = {} - - for name, arg in [ - ("log_level", log_level), - ("debug", debug), - ("host", host), - ("port", port), - ("sse_path", sse_path), - ("message_path", message_path), - ("streamable_http_path", streamable_http_path), - ("json_response", json_response), - ("stateless_http", stateless_http), - ]: - if arg is not None: - # Deprecated in 2.8.0 - if fastmcp.settings.deprecation_warnings: - warnings.warn( - f"Providing `{name}` when creating a server is deprecated. Provide it when calling `run` or as a global setting instead.", - DeprecationWarning, - stacklevel=2, - ) - deprecated_settings[name] = arg - - combined_settings = fastmcp.settings.model_dump() | deprecated_settings - self._deprecated_settings = Settings(**combined_settings) - - @property - def settings(self) -> Settings: - # Deprecated in 2.8.0 - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "Accessing `.settings` on a FastMCP instance is deprecated. Use the global `fastmcp.settings` instead.", - DeprecationWarning, - stacklevel=2, - ) - return self._deprecated_settings - @property def name(self) -> str: return self._mcp_server.name @@ -1530,7 +1401,6 @@ class FastMCP( meta=meta, task=task if task is not None else self._support_tasks_by_default, timeout=timeout, - serializer=self._tool_serializer, auth=auth, ) diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index 0257e8d3b..561a80437 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -2,7 +2,6 @@ from __future__ import annotations as _annotations import inspect import os -import warnings from datetime import timedelta from pathlib import Path from typing import Annotated, Any, Literal @@ -115,30 +114,6 @@ class DocketSettings(BaseSettings): ] = timedelta(seconds=5) -class ExperimentalSettings(BaseSettings): - model_config = SettingsConfigDict( - env_prefix="FASTMCP_EXPERIMENTAL_", - extra="ignore", - validate_assignment=True, - ) - - # Deprecated in 2.14 - the new OpenAPI parser is now the default and only parser - enable_new_openapi_parser: bool = False - - @field_validator("enable_new_openapi_parser", mode="after") - @classmethod - def _warn_openapi_parser_deprecated(cls, v: bool) -> bool: - if v: - warnings.warn( - "enable_new_openapi_parser is deprecated. " - "The new OpenAPI parser is now the default (and only) parser. " - "You can remove this setting.", - DeprecationWarning, - stacklevel=2, - ) - return v - - class Settings(BaseSettings): """FastMCP settings.""" @@ -191,8 +166,6 @@ class Settings(BaseSettings): return v.upper() return v - experimental: ExperimentalSettings = ExperimentalSettings() - docket: DocketSettings = DocketSettings() enable_rich_logging: Annotated[ diff --git a/tests/deprecated/server/test_include_exclude_tags.py b/tests/deprecated/server/test_include_exclude_tags.py index 1bdf68432..70312b412 100644 --- a/tests/deprecated/server/test_include_exclude_tags.py +++ b/tests/deprecated/server/test_include_exclude_tags.py @@ -1,68 +1,25 @@ -"""Tests for deprecated include_tags/exclude_tags parameters.""" +"""Tests for removed include_tags/exclude_tags parameters.""" import pytest from fastmcp import FastMCP -from fastmcp.server.transforms.visibility import Visibility -class TestIncludeExcludeTagsDeprecation: - """Test that include_tags/exclude_tags emit deprecation warnings but still work.""" +class TestIncludeExcludeTagsRemoved: + """Test that include_tags/exclude_tags raise TypeError with migration hints.""" - def test_exclude_tags_emits_warning(self): - """exclude_tags parameter emits deprecation warning.""" - with pytest.warns(DeprecationWarning, match="exclude_tags.*deprecated"): + def test_exclude_tags_raises_type_error(self): + with pytest.raises(TypeError, match="no longer accepts `exclude_tags`"): FastMCP(exclude_tags={"internal"}) - def test_include_tags_emits_warning(self): - """include_tags parameter emits deprecation warning.""" - with pytest.warns(DeprecationWarning, match="include_tags.*deprecated"): + def test_include_tags_raises_type_error(self): + with pytest.raises(TypeError, match="no longer accepts `include_tags`"): FastMCP(include_tags={"public"}) - def test_exclude_tags_still_works(self): - """exclude_tags adds a Visibility transform that disables matching tags.""" - with pytest.warns(DeprecationWarning): - mcp = FastMCP(exclude_tags={"internal"}) + def test_exclude_tags_error_mentions_disable(self): + with pytest.raises(TypeError, match="server.disable"): + FastMCP(exclude_tags={"internal"}) - # Should have added a Visibility transform that disables the tag - enabled_transforms = [t for t in mcp._transforms if isinstance(t, Visibility)] - assert len(enabled_transforms) == 1 - e = enabled_transforms[0] - assert e._enabled is False - assert e.tags == {"internal"} - - def test_include_tags_still_works(self): - """include_tags adds Visibility transforms for allowlist mode.""" - with pytest.warns(DeprecationWarning): - mcp = FastMCP(include_tags={"public"}) - - # Should have added Visibility transforms for allowlist mode - # (one to disable all, one to enable matching) - enabled_transforms = [t for t in mcp._transforms if isinstance(t, Visibility)] - assert len(enabled_transforms) == 2 - - # First should disable all (Visibility.all(False)) - disable_all_transform = enabled_transforms[0] - assert disable_all_transform._enabled is False - assert disable_all_transform.match_all is True - - # Second should enable matching tags - enable_transform = enabled_transforms[1] - assert enable_transform._enabled is True - assert enable_transform.tags == {"public"} - - def test_exclude_and_include_both_create_transforms(self): - """exclude_tags and include_tags both create transforms.""" - with pytest.warns(DeprecationWarning): - mcp = FastMCP(include_tags={"public"}, exclude_tags={"deprecated"}) - - # Should have added transforms for both - # include_tags creates 2 (disable all + enable matching) - # exclude_tags creates 1 (disable matching) - enabled_transforms = [t for t in mcp._transforms if isinstance(t, Visibility)] - assert len(enabled_transforms) == 3 - - # Check we have both tag rules - tags_in_transforms = [t.tags for t in enabled_transforms if t.tags] - assert {"public"} in tags_in_transforms - assert {"deprecated"} in tags_in_transforms + def test_include_tags_error_mentions_enable(self): + with pytest.raises(TypeError, match="server.enable"): + FastMCP(include_tags={"public"}) diff --git a/tests/deprecated/test_add_tool_transformation.py b/tests/deprecated/test_add_tool_transformation.py index 348247b9f..0228b8b60 100644 --- a/tests/deprecated/test_add_tool_transformation.py +++ b/tests/deprecated/test_add_tool_transformation.py @@ -68,37 +68,12 @@ class TestAddToolTransformationDeprecated: assert "remove_tool_transformation is deprecated" in str(w[0].message) assert "no effect" in str(w[0].message) - async def test_tool_transformations_constructor_emits_warning(self): - """tool_transformations constructor param should emit deprecation warning.""" - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + async def test_tool_transformations_constructor_raises_type_error(self): + """tool_transformations constructor param should raise TypeError.""" + import pytest + + with pytest.raises(TypeError, match="no longer accepts `tool_transformations`"): FastMCP( "test", tool_transformations={"my_tool": ToolTransformConfig(name="renamed")}, ) - - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "tool_transformations parameter is deprecated" in str(w[0].message) - - async def test_tool_transformations_constructor_still_works(self): - """tool_transformations constructor param should still apply transforms.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - mcp = FastMCP( - "test", - tool_transformations={ - "my_tool": ToolTransformConfig(name="renamed_tool") - }, - ) - - @mcp.tool - def my_tool() -> str: - return "result" - - async with Client(mcp) as client: - tools = await client.list_tools() - tool_names = [t.name for t in tools] - - assert "my_tool" not in tool_names - assert "renamed_tool" in tool_names diff --git a/tests/deprecated/test_deprecated.py b/tests/deprecated/test_deprecated.py index 25a32aff6..37f33cf27 100644 --- a/tests/deprecated/test_deprecated.py +++ b/tests/deprecated/test_deprecated.py @@ -1,48 +1,23 @@ -import warnings - import pytest from starlette.applications import Starlette from fastmcp import FastMCP -from fastmcp.utilities.tests import temporary_settings - -# reset deprecation warnings for this module -pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning") -class TestDeprecationWarningsSetting: - def test_deprecation_warnings_setting_true(self): - with temporary_settings(deprecation_warnings=True): - with pytest.warns(DeprecationWarning) as recorded_warnings: - # will warn once for providing deprecated arg - mcp = FastMCP(host="1.2.3.4") - # will warn once for accessing deprecated property - mcp.settings +class TestRemovedKwargs: + def test_host_kwarg_raises_type_error(self): + with pytest.raises(TypeError, match="no longer accepts `host`"): + FastMCP(host="1.2.3.4") - assert len(recorded_warnings) == 2 - - def test_deprecation_warnings_setting_false(self): - with temporary_settings(deprecation_warnings=False): - # will error if a warning is raised - with warnings.catch_warnings(): - warnings.simplefilter("error") - # will warn once for providing deprecated arg - mcp = FastMCP(host="1.2.3.4") - # will warn once for accessing deprecated property - mcp.settings + def test_settings_property_removed(self): + mcp = FastMCP() + assert not hasattr(mcp, "_deprecated_settings") + with pytest.raises(AttributeError): + mcp.settings # noqa: B018 # ty: ignore[unresolved-attribute] def test_http_app_with_sse_transport(): - """Test that http_app with SSE transport works (no warning).""" + """Test that http_app with SSE transport works.""" server = FastMCP("TestServer") - - # This should not raise a warning since we're using the new API - with warnings.catch_warnings(record=True) as recorded_warnings: - app = server.http_app(transport="sse") - assert isinstance(app, Starlette) - - # Verify no deprecation warnings were raised for using transport parameter - deprecation_warnings = [ - w for w in recorded_warnings if issubclass(w.category, DeprecationWarning) - ] - assert len(deprecation_warnings) == 0 + app = server.http_app(transport="sse") + assert isinstance(app, Starlette) diff --git a/tests/deprecated/test_openapi_deprecations.py b/tests/deprecated/test_openapi_deprecations.py index d55f84736..b57611c6f 100644 --- a/tests/deprecated/test_openapi_deprecations.py +++ b/tests/deprecated/test_openapi_deprecations.py @@ -5,34 +5,9 @@ import warnings import pytest -import fastmcp - pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning") -class TestEnableNewOpenAPIParserDeprecation: - """Test enable_new_openapi_parser setting deprecation.""" - - def test_setting_true_emits_warning(self): - """Setting enable_new_openapi_parser=True should emit deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match=r"enable_new_openapi_parser is deprecated.*now the default", - ): - fastmcp.settings.experimental.enable_new_openapi_parser = True - - def test_setting_false_no_warning(self): - """Setting enable_new_openapi_parser=False should not emit warning.""" - with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter("always") - fastmcp.settings.experimental.enable_new_openapi_parser = False - - deprecation_warnings = [ - w for w in recorded if issubclass(w.category, DeprecationWarning) - ] - assert len(deprecation_warnings) == 0 - - class TestExperimentalOpenAPIImportDeprecation: """Test experimental OpenAPI import path deprecations.""" diff --git a/tests/deprecated/test_settings.py b/tests/deprecated/test_settings.py index 301abf410..47ea6613c 100644 --- a/tests/deprecated/test_settings.py +++ b/tests/deprecated/test_settings.py @@ -1,319 +1,64 @@ -import warnings -from unittest.mock import patch - import pytest from fastmcp import FastMCP -# reset deprecation warnings for this module -pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning") +class TestRemovedServerInitKwargs: + """Test that removed server initialization keyword arguments raise TypeError.""" -class TestDeprecatedServerInitKwargs: - """Test deprecated server initialization keyword arguments.""" + @pytest.mark.parametrize( + "kwarg, value, expected_message", + [ + ("host", "0.0.0.0", "run_http_async"), + ("port", 8080, "run_http_async"), + ("sse_path", "/custom-sse", "FASTMCP_SSE_PATH"), + ("message_path", "/custom-message", "FASTMCP_MESSAGE_PATH"), + ("streamable_http_path", "/custom-http", "run_http_async"), + ("json_response", True, "run_http_async"), + ("stateless_http", True, "run_http_async"), + ("debug", True, "FASTMCP_DEBUG"), + ("log_level", "DEBUG", "run_http_async"), + ("on_duplicate_tools", "warn", "on_duplicate="), + ("on_duplicate_resources", "error", "on_duplicate="), + ("on_duplicate_prompts", "replace", "on_duplicate="), + ("tool_serializer", lambda x: str(x), "ToolResult"), + ("include_tags", {"public"}, "server.enable"), + ("exclude_tags", {"internal"}, "server.disable"), + ( + "tool_transformations", + {"my_tool": {"name": "renamed"}}, + "server.add_transform", + ), + ], + ) + def test_removed_kwarg_raises_type_error(self, kwarg, value, expected_message): + with pytest.raises(TypeError, match=f"no longer accepts `{kwarg}`"): + FastMCP("TestServer", **{kwarg: value}) - def test_log_level_deprecation_warning(self): - """Test that log_level raises a deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match=r"Providing `log_level` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.", - ): - server = FastMCP("TestServer", log_level="DEBUG") + @pytest.mark.parametrize( + "kwarg, value, expected_message", + [ + ("host", "0.0.0.0", "run_http_async"), + ("on_duplicate_tools", "warn", "on_duplicate="), + ("include_tags", {"public"}, "server.enable"), + ], + ) + def test_removed_kwarg_error_includes_migration_hint( + self, kwarg, value, expected_message + ): + with pytest.raises(TypeError, match=expected_message): + FastMCP("TestServer", **{kwarg: value}) - # Verify the setting is still applied - assert server._deprecated_settings.log_level == "DEBUG" + def test_unknown_kwarg_raises_standard_type_error(self): + with pytest.raises(TypeError, match="unexpected keyword argument"): + FastMCP("TestServer", **{"totally_fake_param": True}) # ty: ignore[invalid-argument-type] - def test_debug_deprecation_warning(self): - """Test that debug raises a deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match=r"Providing `debug` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.", - ): - server = FastMCP("TestServer", debug=True) - - # Verify the setting is still applied - assert server._deprecated_settings.debug is True - - def test_host_deprecation_warning(self): - """Test that host raises a deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match=r"Providing `host` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.", - ): - server = FastMCP("TestServer", host="0.0.0.0") - - # Verify the setting is still applied - assert server._deprecated_settings.host == "0.0.0.0" - - def test_port_deprecation_warning(self): - """Test that port raises a deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match=r"Providing `port` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.", - ): - server = FastMCP("TestServer", port=8080) - - # Verify the setting is still applied - assert server._deprecated_settings.port == 8080 - - def test_sse_path_deprecation_warning(self): - """Test that sse_path raises a deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match=r"Providing `sse_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.", - ): - server = FastMCP("TestServer", sse_path="/custom-sse") - - # Verify the setting is still applied - assert server._deprecated_settings.sse_path == "/custom-sse" - - def test_message_path_deprecation_warning(self): - """Test that message_path raises a deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match=r"Providing `message_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.", - ): - server = FastMCP("TestServer", message_path="/custom-message") - - # Verify the setting is still applied - assert server._deprecated_settings.message_path == "/custom-message" - - def test_streamable_http_path_deprecation_warning(self): - """Test that streamable_http_path raises a deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match=r"Providing `streamable_http_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.", - ): - server = FastMCP("TestServer", streamable_http_path="/custom-http") - - # Verify the setting is still applied - assert server._deprecated_settings.streamable_http_path == "/custom-http" - - def test_json_response_deprecation_warning(self): - """Test that json_response raises a deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match=r"Providing `json_response` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.", - ): - server = FastMCP("TestServer", json_response=True) - - # Verify the setting is still applied - assert server._deprecated_settings.json_response is True - - def test_stateless_http_deprecation_warning(self): - """Test that stateless_http raises a deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match=r"Providing `stateless_http` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.", - ): - server = FastMCP("TestServer", stateless_http=True) - - # Verify the setting is still applied - assert server._deprecated_settings.stateless_http is True - - def test_multiple_deprecated_kwargs_warnings(self): - """Test that multiple deprecated kwargs each raise their own warning.""" - with warnings.catch_warnings(record=True) as recorded_warnings: - warnings.simplefilter("always") - server = FastMCP( - "TestServer", - log_level="INFO", - debug=False, - host="127.0.0.1", - port=9999, - sse_path="/sse/", - message_path="/msg", - streamable_http_path="/http", - json_response=False, - stateless_http=False, - ) - - # Should have 9 deprecation warnings (one for each deprecated parameter) - deprecation_warnings = [ - w for w in recorded_warnings if issubclass(w.category, DeprecationWarning) - ] - assert len(deprecation_warnings) == 9 - - # Verify all expected parameters are mentioned in warnings - expected_params = { - "log_level", - "debug", - "host", - "port", - "sse_path", - "message_path", - "streamable_http_path", - "json_response", - "stateless_http", - } - mentioned_params = set() - for warning in deprecation_warnings: - message = str(warning.message) - for param in expected_params: - if f"Providing `{param}`" in message: - mentioned_params.add(param) - - assert mentioned_params == expected_params - - # Verify all settings are still applied - assert server._deprecated_settings.log_level == "INFO" - assert server._deprecated_settings.debug is False - assert server._deprecated_settings.host == "127.0.0.1" - assert server._deprecated_settings.port == 9999 - assert server._deprecated_settings.sse_path == "/sse/" - assert server._deprecated_settings.message_path == "/msg" - assert server._deprecated_settings.streamable_http_path == "/http" - assert server._deprecated_settings.json_response is False - assert server._deprecated_settings.stateless_http is False - - def test_non_deprecated_kwargs_no_warnings(self): - """Test that non-deprecated kwargs don't raise warnings.""" - with warnings.catch_warnings(record=True) as recorded_warnings: - warnings.simplefilter("always") - server = FastMCP( - name="TestServer", - instructions="Test instructions", - on_duplicate="warn", # New unified parameter - mask_error_details=True, - ) - - # Should have no deprecation warnings - deprecation_warnings = [ - w for w in recorded_warnings if issubclass(w.category, DeprecationWarning) - ] - assert len(deprecation_warnings) == 0 - - # Verify server was created successfully + def test_valid_kwargs_still_work(self): + server = FastMCP( + name="TestServer", + instructions="Test instructions", + on_duplicate="warn", + mask_error_details=True, + ) assert server.name == "TestServer" assert server.instructions == "Test instructions" - - def test_deprecated_duplicate_kwargs_raise_warnings(self): - """Test that deprecated on_duplicate_* kwargs raise warnings.""" - with warnings.catch_warnings(record=True) as recorded_warnings: - warnings.simplefilter("always") - FastMCP( - name="TestServer", - on_duplicate_tools="warn", - on_duplicate_resources="error", - on_duplicate_prompts="replace", - ) - - # Should have 3 deprecation warnings (one for each deprecated param) - deprecation_warnings = [ - w for w in recorded_warnings if issubclass(w.category, DeprecationWarning) - ] - assert len(deprecation_warnings) == 3 - - # Check warning messages - warning_messages = [str(w.message) for w in deprecation_warnings] - assert any("on_duplicate_tools" in msg for msg in warning_messages) - assert any("on_duplicate_resources" in msg for msg in warning_messages) - assert any("on_duplicate_prompts" in msg for msg in warning_messages) - - def test_none_values_no_warnings(self): - """Test that None values for deprecated kwargs don't raise warnings.""" - with warnings.catch_warnings(record=True) as recorded_warnings: - warnings.simplefilter("always") - FastMCP( - "TestServer", - log_level=None, - debug=None, - host=None, - port=None, - sse_path=None, - message_path=None, - streamable_http_path=None, - json_response=None, - stateless_http=None, - ) - - # Should have no deprecation warnings for None values - deprecation_warnings = [ - w for w in recorded_warnings if issubclass(w.category, DeprecationWarning) - ] - assert len(deprecation_warnings) == 0 - - def test_deprecated_settings_inheritance_from_global(self): - """Test that deprecated settings inherit from global settings when not provided.""" - # Mock fastmcp.settings to test inheritance - with patch("fastmcp.settings") as mock_settings: - mock_settings.model_dump.return_value = { - "log_level": "WARNING", - "debug": True, - "host": "0.0.0.0", - "port": 3000, - "sse_path": "/events", - "message_path": "/messages", - "streamable_http_path": "/stream", - "json_response": True, - "stateless_http": True, - } - - server = FastMCP("TestServer") - - # Verify settings are inherited from global settings - assert server._deprecated_settings.log_level == "WARNING" - assert server._deprecated_settings.debug is True - assert server._deprecated_settings.host == "0.0.0.0" - assert server._deprecated_settings.port == 3000 - assert server._deprecated_settings.sse_path == "/events" - assert server._deprecated_settings.message_path == "/messages" - assert server._deprecated_settings.streamable_http_path == "/stream" - assert server._deprecated_settings.json_response is True - assert server._deprecated_settings.stateless_http is True - - def test_deprecated_settings_override_global(self): - """Test that deprecated settings override global settings when provided.""" - # Mock fastmcp.settings to test override behavior - with patch("fastmcp.settings") as mock_settings: - mock_settings.model_dump.return_value = { - "log_level": "WARNING", - "debug": True, - "host": "0.0.0.0", - "port": 3000, - "sse_path": "/events", - "message_path": "/messages", - "streamable_http_path": "/stream", - "json_response": True, - "stateless_http": True, - } - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") # Ignore warnings for this test - server = FastMCP( - "TestServer", - log_level="ERROR", - debug=False, - host="127.0.0.1", - port=8080, - ) - - # Verify provided settings override global settings - assert server._deprecated_settings.log_level == "ERROR" - assert server._deprecated_settings.debug is False - assert server._deprecated_settings.host == "127.0.0.1" - assert server._deprecated_settings.port == 8080 - # Non-overridden settings should still come from global - assert server._deprecated_settings.sse_path == "/events" - assert server._deprecated_settings.message_path == "/messages" - assert server._deprecated_settings.streamable_http_path == "/stream" - assert server._deprecated_settings.json_response is True - assert server._deprecated_settings.stateless_http is True - - def test_stacklevel_points_to_constructor_call(self): - """Test that deprecation warnings point to the FastMCP constructor call.""" - with warnings.catch_warnings(record=True) as recorded_warnings: - warnings.simplefilter("always") - - FastMCP("TestServer", log_level="DEBUG") - - # Should have exactly one deprecation warning - deprecation_warnings = [ - w for w in recorded_warnings if issubclass(w.category, DeprecationWarning) - ] - assert len(deprecation_warnings) == 1 - - # The warning should point to the server.py file where FastMCP.__init__ is called - # This verifies the stacklevel is working as intended (pointing to constructor) - warning = deprecation_warnings[0] - assert "server.py" in warning.filename diff --git a/tests/deprecated/test_tool_serializer.py b/tests/deprecated/test_tool_serializer.py index f90bf06cc..2b706ae74 100644 --- a/tests/deprecated/test_tool_serializer.py +++ b/tests/deprecated/test_tool_serializer.py @@ -143,15 +143,14 @@ class TestSerializerDeprecationWarnings: with pytest.warns(DeprecationWarning, match="serializer.*deprecated"): provider.tool(my_tool, serializer=custom_serializer) - def test_fastmcp_tool_serializer_parameter_warning(self): - """Test that FastMCP tool_serializer parameter warns.""" + 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 temporary_settings(deprecation_warnings=True): - with pytest.warns(DeprecationWarning, match="tool_serializer.*deprecated"): - FastMCP("TestServer", tool_serializer=custom_serializer) + 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.""" diff --git a/tests/server/mount/test_filtering.py b/tests/server/mount/test_filtering.py index 413cc0e60..376db4c75 100644 --- a/tests/server/mount/test_filtering.py +++ b/tests/server/mount/test_filtering.py @@ -11,7 +11,8 @@ class TestParentTagFiltering: async def test_parent_include_tags_filters_mounted_tools(self): """Test that parent include_tags filters out non-matching mounted tools.""" - parent = FastMCP("Parent", include_tags={"allowed"}) + parent = FastMCP("Parent") + parent.enable(tags={"allowed"}, only=True) mounted = FastMCP("Mounted") @mounted.tool(tags={"allowed"}) @@ -38,7 +39,8 @@ class TestParentTagFiltering: async def test_parent_exclude_tags_filters_mounted_tools(self): """Test that parent exclude_tags filters out matching mounted tools.""" - parent = FastMCP("Parent", exclude_tags={"blocked"}) + parent = FastMCP("Parent") + parent.disable(tags={"blocked"}) mounted = FastMCP("Mounted") @mounted.tool(tags={"production"}) @@ -58,7 +60,8 @@ class TestParentTagFiltering: async def test_parent_filters_apply_to_mounted_resources(self): """Test that parent tag filters apply to mounted resources.""" - parent = FastMCP("Parent", include_tags={"allowed"}) + parent = FastMCP("Parent") + parent.enable(tags={"allowed"}, only=True) mounted = FastMCP("Mounted") @mounted.resource("resource://allowed", tags={"allowed"}) @@ -78,7 +81,8 @@ class TestParentTagFiltering: async def test_parent_filters_apply_to_mounted_prompts(self): """Test that parent tag filters apply to mounted prompts.""" - parent = FastMCP("Parent", exclude_tags={"blocked"}) + parent = FastMCP("Parent") + parent.disable(tags={"blocked"}) mounted = FastMCP("Mounted") @mounted.prompt(tags={"allowed"}) diff --git a/tests/server/providers/local_provider_tools/test_tags.py b/tests/server/providers/local_provider_tools/test_tags.py index 019ed4c07..fb32e3515 100644 --- a/tests/server/providers/local_provider_tools/test_tags.py +++ b/tests/server/providers/local_provider_tools/test_tags.py @@ -40,7 +40,7 @@ class PersonDataclass: class TestToolTags: def create_server(self, include_tags=None, exclude_tags=None): - mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags) + mcp = FastMCP() @mcp.tool(tags={"a", "b"}) def tool_1() -> int: @@ -50,6 +50,11 @@ class TestToolTags: def tool_2() -> int: return 2 + if include_tags: + mcp.enable(tags=include_tags, only=True) + if exclude_tags: + mcp.disable(tags=exclude_tags) + return mcp async def test_include_tags_all_tools(self): diff --git a/tests/server/providers/test_local_provider_prompts.py b/tests/server/providers/test_local_provider_prompts.py index 6b0400e64..ac0df86a3 100644 --- a/tests/server/providers/test_local_provider_prompts.py +++ b/tests/server/providers/test_local_provider_prompts.py @@ -415,7 +415,7 @@ class TestPromptEnabled: class TestPromptTags: def create_server(self, include_tags=None, exclude_tags=None): - mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags) + mcp = FastMCP() @mcp.prompt(tags={"a", "b"}) def prompt_1() -> str: @@ -425,6 +425,11 @@ class TestPromptTags: def prompt_2() -> str: return "2" + if include_tags: + mcp.enable(tags=include_tags, only=True) + if exclude_tags: + mcp.disable(tags=exclude_tags) + return mcp async def test_include_tags_all_prompts(self): diff --git a/tests/server/providers/test_local_provider_resources.py b/tests/server/providers/test_local_provider_resources.py index 972b9358b..4c8da4559 100644 --- a/tests/server/providers/test_local_provider_resources.py +++ b/tests/server/providers/test_local_provider_resources.py @@ -676,7 +676,7 @@ class TestTemplateDecorator: class TestResourceTags: def create_server(self, include_tags=None, exclude_tags=None): - mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags) + mcp = FastMCP() @mcp.resource("resource://1", tags={"a", "b"}) def resource_1() -> str: @@ -686,6 +686,11 @@ class TestResourceTags: def resource_2() -> str: return "2" + if include_tags: + mcp.enable(tags=include_tags, only=True) + if exclude_tags: + mcp.disable(tags=exclude_tags) + return mcp async def test_include_tags_all_resources(self): @@ -823,7 +828,7 @@ class TestResourceEnabled: class TestResourceTemplatesTags: def create_server(self, include_tags=None, exclude_tags=None): - mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags) + mcp = FastMCP() @mcp.resource("resource://1/{param}", tags={"a", "b"}) def template_resource_1(param: str) -> str: @@ -833,6 +838,11 @@ class TestResourceTemplatesTags: def template_resource_2(param: str) -> str: return f"Template resource 2: {param}" + if include_tags: + mcp.enable(tags=include_tags, only=True) + if exclude_tags: + mcp.disable(tags=exclude_tags) + return mcp async def test_include_tags_all_resources(self): diff --git a/tests/server/test_server.py b/tests/server/test_server.py index 12166715c..ba7af98d5 100644 --- a/tests/server/test_server.py +++ b/tests/server/test_server.py @@ -211,9 +211,9 @@ class TestAbstractCollectionTypes: "test", middleware=(), # Empty tuple tools=(Tool.from_function(dummy_tool),), # Tuple of tools - include_tags={"tag1", "tag2"}, # Set - exclude_tags={"tag3"}, # Set ) + mcp.enable(tags={"tag1", "tag2"}, only=True) + mcp.disable(tags={"tag3"}) assert mcp is not None assert mcp.name == "test" assert isinstance(mcp.middleware, list) # Should be converted to list diff --git a/tests/utilities/test_inspect.py b/tests/utilities/test_inspect.py index 8df506466..448e1eb03 100644 --- a/tests/utilities/test_inspect.py +++ b/tests/utilities/test_inspect.py @@ -281,10 +281,8 @@ class TestGetFastMCPInfo: components weren't actually available to clients. """ # Create server with include_tags that will filter out untagged components - mcp = FastMCP( - "FilteredServer", - include_tags={"fetch", "analyze", "create"}, - ) + mcp = FastMCP("FilteredServer") + mcp.enable(tags={"fetch", "analyze", "create"}, only=True) # Add tools with and without matching tags @mcp.tool(tags={"fetch"}) @@ -396,7 +394,8 @@ class TestGetFastMCPInfo: return [{"role": "user", "content": "blocked"}] # Create parent server with tag filtering - parent = FastMCP("ParentServer", include_tags={"allowed"}) + parent = FastMCP("ParentServer") + parent.enable(tags={"allowed"}, only=True) parent.mount(mounted) # Get inspect info @@ -448,7 +447,8 @@ class TestGetFastMCPInfo: return "untagged" # Create parent with exclude_tags - should filter mounted components - parent = FastMCP("ParentServer", exclude_tags={"development"}) + parent = FastMCP("ParentServer") + parent.disable(tags={"development"}) parent.mount(mounted) # Get inspect info