From cd7f9850d313b307866d3d9f1d7da9fe9f89eef9 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:22:28 -0500 Subject: [PATCH 1/3] Remove deprecated FastMCP() constructor kwargs Replace 16 deprecated kwargs with **kwargs catch that raises TypeError with specific migration instructions. Transport methods now read from fastmcp.settings directly. Delete ExperimentalSettings (dead code). --- docs/development/v3-notes/v3-features.mdx | 56 +-- src/fastmcp/mcp_config.py | 7 +- src/fastmcp/server/mixins/transport.py | 25 +- src/fastmcp/server/server.py | 204 ++-------- 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 | 369 +++--------------- 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 +- 17 files changed, 212 insertions(+), 719 deletions(-) diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx index 690390e39..04e6120d8 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_http_async()` or `http_app()`, 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_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 - -`FastMCP(include_tags=..., exclude_tags=...)` deprecated. Use `enable()`/`disable()` methods: +#### Tag Filtering, Tool Serializer, Tool Transformations Init Parameters -```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/src/fastmcp/mcp_config.py b/src/fastmcp/mcp_config.py index f65132edb..98146652d 100644 --- a/src/fastmcp/mcp_config.py +++ b/src/fastmcp/mcp_config.py @@ -109,10 +109,13 @@ def _to_server_and_underlying_transport( wrapped_mcp_server = create_proxy( client, name=server_name, - include_tags=self.include_tags, - exclude_tags=self.exclude_tags, ) + if self.include_tags: + wrapped_mcp_server.enable(tags=self.include_tags, only=True) + if self.exclude_tags: + 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 @@ async def run_http_async( # 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 @@ def http_app( 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 @@ AsyncIterator, Awaitable, Callable, - Collection, - Mapping, Sequence, ) from contextlib import ( @@ -79,7 +77,6 @@ ) 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 @@ 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 @@ 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. - - 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" +_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.", +} + + +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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def my_tool(x: int) -> str: 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 @@ 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 @@ def normalize_log_level(cls, v): 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"}) - - # 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 + def test_exclude_tags_error_mentions_disable(self): + with pytest.raises(TypeError, match="server.disable"): + FastMCP(exclude_tags={"internal"}) - # 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 @@ async def test_remove_tool_transformation_emits_warning(self): 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 - assert len(recorded_warnings) == 2 +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") - 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 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 TestDeprecatedServerInitKwargs: - """Test deprecated server initialization keyword arguments.""" - - 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") - - # Verify the setting is still applied - assert server._deprecated_settings.log_level == "DEBUG" - - 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 +class TestRemovedServerInitKwargs: + """Test that removed server initialization keyword arguments raise TypeError.""" + + @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}) + + @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}) + + 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_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 @@ def my_tool(x: int) -> int: 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 @@ def blocked_tool() -> str: 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 @@ def blocked_tool() -> str: 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 @@ def blocked_resource() -> str: 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 @@ def tool_1() -> int: 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 @@ def sample_prompt() -> str: 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 @@ def prompt_1() -> str: 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 @@ def get_template_data(param: str) -> str: 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 @@ def resource_1() -> str: 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 @@ def sample_resource() -> str: 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 @@ def template_resource_1(param: str) -> str: 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 @@ def dummy_tool() -> str: "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 @@ async def test_inspect_respects_tag_filtering(self): 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 @@ def blocked_prompt() -> list: 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 @@ def untagged_tool() -> str: 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 From 29a37a84321eee65985bd6a0b3331ffbeee81952 Mon Sep 17 00:00:00 2001 From: "marvin-context-protocol[bot]" <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:23:48 +0000 Subject: [PATCH 2/3] chore: Update SDK documentation --- docs/python-sdk/fastmcp-mcp_config.mdx | 32 +++--- docs/python-sdk/fastmcp-server-context.mdx | 76 ++++++------ .../fastmcp-server-dependencies.mdx | 77 +++++++------ .../fastmcp-server-mixins-transport.mdx | 2 +- docs/python-sdk/fastmcp-server-server.mdx | 108 +++++++++--------- docs/python-sdk/fastmcp-settings.mdx | 12 +- 6 files changed, 152 insertions(+), 155 deletions(-) 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-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx index 2873100b7..a87096234 100644 --- a/docs/python-sdk/fastmcp-server-context.mdx +++ b/docs/python-sdk/fastmcp-server-context.mdx @@ -170,6 +170,10 @@ Returns the context dict yielded by the server's lifespan function. Returns an empty dict if no lifespan was configured or if the MCP session is not yet established. +In background tasks (Docket workers), where request_context is not +available, falls back to reading from the FastMCP server's lifespan +result directly. + Example: ```python @server.tool @@ -181,7 +185,7 @@ def my_tool(ctx: Context) -> str: ``` -#### `report_progress` +#### `report_progress` ```python report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None @@ -198,7 +202,7 @@ Works in both foreground (MCP progress notifications) and background - `message`: Optional status message describing current progress -#### `list_resources` +#### `list_resources` ```python list_resources(self) -> list[SDKResource] @@ -210,7 +214,7 @@ List all available resources from the server. - List of Resource objects available on the server -#### `list_prompts` +#### `list_prompts` ```python list_prompts(self) -> list[SDKPrompt] @@ -222,7 +226,7 @@ List all available prompts from the server. - List of Prompt objects available on the server -#### `get_prompt` +#### `get_prompt` ```python get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult @@ -238,7 +242,7 @@ Get a prompt by name with optional arguments. - The prompt result -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: str | AnyUrl) -> ResourceResult @@ -253,7 +257,7 @@ Read a resource by URI. - ResourceResult with contents -#### `log` +#### `log` ```python log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -271,7 +275,7 @@ Messages sent to Clients are also logged to the `fastmcp.server.context.to_clien - `extra`: Optional mapping for additional arguments -#### `transport` +#### `transport` ```python transport(self) -> TransportType | None @@ -283,7 +287,7 @@ Returns the transport type used to run this server: "stdio", "sse", or "streamable-http". Returns None if called outside of a server context. -#### `client_supports_extension` +#### `client_supports_extension` ```python client_supports_extension(self, extension_id: str) -> bool @@ -308,7 +312,7 @@ Example:: return "text-only client" -#### `client_id` +#### `client_id` ```python client_id(self) -> str | None @@ -317,7 +321,7 @@ client_id(self) -> str | None Get the client ID if available. -#### `request_id` +#### `request_id` ```python request_id(self) -> str @@ -328,7 +332,7 @@ Get the unique ID for this request. Raises RuntimeError if MCP request context is not available. -#### `session_id` +#### `session_id` ```python session_id(self) -> str @@ -345,7 +349,7 @@ the same client session. - for other transports. -#### `session` +#### `session` ```python session(self) -> ServerSession @@ -359,7 +363,7 @@ In background task mode: Returns the session stored at Context creation. Raises RuntimeError if no session is available. -#### `debug` +#### `debug` ```python debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -370,7 +374,7 @@ Send a `DEBUG`-level message to the connected MCP Client. Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. -#### `info` +#### `info` ```python info(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -381,7 +385,7 @@ Send a `INFO`-level message to the connected MCP Client. Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. -#### `warning` +#### `warning` ```python warning(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -392,7 +396,7 @@ Send a `WARNING`-level message to the connected MCP Client. Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. -#### `error` +#### `error` ```python error(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -403,7 +407,7 @@ Send a `ERROR`-level message to the connected MCP Client. Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. -#### `list_roots` +#### `list_roots` ```python list_roots(self) -> list[Root] @@ -412,7 +416,7 @@ list_roots(self) -> list[Root] List the roots available to the server, as indicated by the client. -#### `send_notification` +#### `send_notification` ```python send_notification(self, notification: mcp.types.ServerNotificationType) -> None @@ -424,7 +428,7 @@ Send a notification to the client immediately. - `notification`: An MCP notification instance (e.g., ToolListChangedNotification()) -#### `close_sse_stream` +#### `close_sse_stream` ```python close_sse_stream(self) -> None @@ -442,7 +446,7 @@ Instead of holding a connection open for minutes, you can periodically close and let the client reconnect. -#### `sample_step` +#### `sample_step` ```python sample_step(self, messages: str | Sequence[str | SamplingMessage]) -> SampleStep @@ -485,7 +489,7 @@ regardless of this setting. - - .text: The text content (if any) -#### `sample` +#### `sample` ```python sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT] @@ -494,7 +498,7 @@ sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ Overload: With result_type, returns SamplingResult[ResultT]. -#### `sample` +#### `sample` ```python sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[str] @@ -503,7 +507,7 @@ sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ Overload: Without result_type, returns SamplingResult[str]. -#### `sample` +#### `sample` ```python sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT] | SamplingResult[str] @@ -551,43 +555,43 @@ regardless of this setting. - - .history: All messages exchanged during sampling -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: None) -> AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: dict[str, dict[str, str]]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: list[list[str]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: list[dict[str, dict[str, str]]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: type[T] | list[str] | dict[str, dict[str, str]] | list[list[str]] | list[dict[str, dict[str, str]]] | None = None) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation @@ -616,7 +620,7 @@ type or dataclass or BaseModel. If it is a primitive type, an object schema with a single "value" field will be generated. -#### `set_state` +#### `set_state` ```python set_state(self, key: str, value: Any) -> None @@ -629,7 +633,7 @@ The key is automatically prefixed with the session identifier. State expires after 1 day to prevent unbounded memory growth. -#### `get_state` +#### `get_state` ```python get_state(self, key: str) -> Any @@ -640,7 +644,7 @@ Get a value from the session-scoped state store. Returns None if the key is not found. -#### `delete_state` +#### `delete_state` ```python delete_state(self, key: str) -> None @@ -649,7 +653,7 @@ delete_state(self, key: str) -> None Delete a value from the session-scoped state store. -#### `enable_components` +#### `enable_components` ```python enable_components(self) -> None @@ -673,7 +677,7 @@ ResourceListChangedNotification, and PromptListChangedNotification. - `match_all`: If True, matches all components regardless of other criteria. -#### `disable_components` +#### `disable_components` ```python disable_components(self) -> None @@ -697,7 +701,7 @@ ResourceListChangedNotification, and PromptListChangedNotification. - `match_all`: If True, matches all components regardless of other criteria. -#### `reset_visibility` +#### `reset_visibility` ```python reset_visibility(self) -> None diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx index 496a19e8c..5660ac13a 100644 --- a/docs/python-sdk/fastmcp-server-dependencies.mdx +++ b/docs/python-sdk/fastmcp-server-dependencies.mdx @@ -15,7 +15,7 @@ CurrentWorker) and background task execution require fastmcp[tasks]. ## Functions -### `get_task_context` +### `get_task_context` ```python get_task_context() -> TaskContextInfo | None @@ -31,7 +31,7 @@ Returns None if not running in a task context (e.g., foreground execution). - TaskContextInfo with task_id and session_id, or None if not in a task. -### `register_task_session` +### `register_task_session` ```python register_task_session(session_id: str, session: ServerSession) -> None @@ -49,7 +49,7 @@ client disconnects. - `session`: The ServerSession instance -### `get_task_session` +### `get_task_session` ```python get_task_session(session_id: str) -> ServerSession | None @@ -65,7 +65,7 @@ Get a registered session by ID if still alive. - The ServerSession if found and alive, None otherwise -### `is_docket_available` +### `is_docket_available` ```python is_docket_available() -> bool @@ -75,7 +75,7 @@ is_docket_available() -> bool Check if pydocket is installed. -### `require_docket` +### `require_docket` ```python require_docket(feature: str) -> None @@ -89,7 +89,7 @@ Raise ImportError with install instructions if docket not available. "CurrentDocket()"). Will be included in the error message. -### `transform_context_annotations` +### `transform_context_annotations` ```python transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any] @@ -115,7 +115,7 @@ allows them to have defaults in any order. - Function with modified signature (same function object, updated __signature__) -### `get_context` +### `get_context` ```python get_context() -> Context @@ -125,7 +125,7 @@ get_context() -> Context Get the current FastMCP Context instance directly. -### `get_server` +### `get_server` ```python get_server() -> FastMCP @@ -141,7 +141,7 @@ Get the current FastMCP server instance directly. - `RuntimeError`: If no server in context -### `get_http_request` +### `get_http_request` ```python get_http_request() -> Request @@ -153,7 +153,7 @@ Get the current HTTP request. Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context. -### `get_http_headers` +### `get_http_headers` ```python get_http_headers(include_all: bool = False) -> dict[str, str] @@ -169,7 +169,7 @@ By default, strips problematic headers like `content-length` that cause issues if forwarded to downstream clients. If `include_all` is True, all headers are returned. -### `get_access_token` +### `get_access_token` ```python get_access_token() -> AccessToken | None @@ -181,13 +181,14 @@ Get the FastMCP access token from the current context. This function first tries to get the token from the current HTTP request's scope, which is more reliable for long-lived connections where the SDK's auth_context_var may become stale after token refresh. Falls back to the SDK's context var if no -request is available. +request is available. In background tasks (Docket workers), falls back to the +token snapshot stored in Redis at task submission time. **Returns:** - The access token if an authenticated user is available, None otherwise. -### `without_injected_parameters` +### `without_injected_parameters` ```python without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any] @@ -212,7 +213,7 @@ Handles: - Async wrapper function without injected parameters -### `resolve_dependencies` +### `resolve_dependencies` ```python resolve_dependencies(fn: Callable[..., Any], arguments: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None] @@ -238,7 +239,7 @@ time, so all injection goes through the unified DI system. which will be filtered out) -### `CurrentContext` +### `CurrentContext` ```python CurrentContext() -> Context @@ -257,7 +258,7 @@ current MCP operation (tool/resource/prompt call). - `RuntimeError`: If no active context found (during resolution) -### `CurrentDocket` +### `CurrentDocket` ```python CurrentDocket() -> Docket @@ -277,7 +278,7 @@ automatically creates for background task scheduling. - `ImportError`: If fastmcp[tasks] not installed -### `CurrentWorker` +### `CurrentWorker` ```python CurrentWorker() -> Worker @@ -297,7 +298,7 @@ automatically creates for background task processing. - `ImportError`: If fastmcp[tasks] not installed -### `CurrentFastMCP` +### `CurrentFastMCP` ```python CurrentFastMCP() -> FastMCP @@ -315,7 +316,7 @@ This dependency provides access to the active FastMCP server. - `RuntimeError`: If no server in context (during resolution) -### `CurrentRequest` +### `CurrentRequest` ```python CurrentRequest() -> Request @@ -335,7 +336,7 @@ current HTTP request. Only available when running over HTTP transports - `RuntimeError`: If no HTTP request in context (e.g., STDIO transport) -### `CurrentHeaders` +### `CurrentHeaders` ```python CurrentHeaders() -> dict[str, str] @@ -352,7 +353,7 @@ safe to use in code that might run over any transport. - A dependency that resolves to a dictionary of header name -> value -### `CurrentAccessToken` +### `CurrentAccessToken` ```python CurrentAccessToken() -> AccessToken @@ -371,7 +372,7 @@ authenticated request. Raises an error if no authentication is present. - `RuntimeError`: If no authenticated user (use get_access_token() for optional) -### `TokenClaim` +### `TokenClaim` ```python TokenClaim(name: str) -> str @@ -396,7 +397,7 @@ without needing the full token object. ## Classes -### `TaskContextInfo` +### `TaskContextInfo` Information about the current background task context. @@ -405,7 +406,7 @@ Returned by ``get_task_context()`` when running inside a Docket worker. Contains identifiers needed to communicate with the MCP session. -### `ProgressLike` +### `ProgressLike` Protocol for progress tracking interface. @@ -416,7 +417,7 @@ and Docket's Progress (worker context). **Methods:** -#### `current` +#### `current` ```python current(self) -> int | None @@ -425,7 +426,7 @@ current(self) -> int | None Current progress value. -#### `total` +#### `total` ```python total(self) -> int @@ -434,7 +435,7 @@ total(self) -> int Total/target progress value. -#### `message` +#### `message` ```python message(self) -> str | None @@ -443,7 +444,7 @@ message(self) -> str | None Current progress message. -#### `set_total` +#### `set_total` ```python set_total(self, total: int) -> None @@ -452,7 +453,7 @@ set_total(self, total: int) -> None Set the total/target value for progress tracking. -#### `increment` +#### `increment` ```python increment(self, amount: int = 1) -> None @@ -461,7 +462,7 @@ increment(self, amount: int = 1) -> None Atomically increment the current progress value. -#### `set_message` +#### `set_message` ```python set_message(self, message: str | None) -> None @@ -470,7 +471,7 @@ set_message(self, message: str | None) -> None Update the progress status message. -### `InMemoryProgress` +### `InMemoryProgress` In-memory progress tracker for immediate tool execution. @@ -482,25 +483,25 @@ progress doesn't need to be observable across processes. **Methods:** -#### `current` +#### `current` ```python current(self) -> int | None ``` -#### `total` +#### `total` ```python total(self) -> int ``` -#### `message` +#### `message` ```python message(self) -> str | None ``` -#### `set_total` +#### `set_total` ```python set_total(self, total: int) -> None @@ -509,7 +510,7 @@ set_total(self, total: int) -> None Set the total/target value for progress tracking. -#### `increment` +#### `increment` ```python increment(self, amount: int = 1) -> None @@ -518,7 +519,7 @@ increment(self, amount: int = 1) -> None Atomically increment the current progress value. -#### `set_message` +#### `set_message` ```python set_message(self, message: str | None) -> None @@ -527,7 +528,7 @@ set_message(self, message: str | None) -> None Update the progress status message. -### `Progress` +### `Progress` FastMCP Progress dependency that works in both server and worker contexts. 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) From ac00017028f9219271907d007e6d0c0d3d68fc0d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:32:00 -0500 Subject: [PATCH 3/3] Fix review feedback: is not None checks, docs method name --- docs/development/v3-notes/v3-features.mdx | 4 ++-- src/fastmcp/mcp_config.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx index 04e6120d8..93420c6b8 100644 --- a/docs/development/v3-notes/v3-features.mdx +++ b/docs/development/v3-notes/v3-features.mdx @@ -70,7 +70,7 @@ Background tasks now use a distributed Redis notification queue for reliable del 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_http_async()` or `http_app()`, or set via environment variables. +**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 @@ -79,7 +79,7 @@ mcp.run() # After mcp = FastMCP("server") -mcp.run_http(host="0.0.0.0", port=8080) +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. diff --git a/src/fastmcp/mcp_config.py b/src/fastmcp/mcp_config.py index 98146652d..c7dac539f 100644 --- a/src/fastmcp/mcp_config.py +++ b/src/fastmcp/mcp_config.py @@ -111,9 +111,9 @@ def _to_server_and_underlying_transport( name=server_name, ) - if self.include_tags: + if self.include_tags is not None: wrapped_mcp_server.enable(tags=self.include_tags, only=True) - if self.exclude_tags: + if self.exclude_tags is not None: wrapped_mcp_server.disable(tags=self.exclude_tags) # Apply tool transforms if configured