From 5d3d97b220d9bd052e4beb1f0acbd50fe6add9a8 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:27:37 -0400 Subject: [PATCH 1/3] Expose stateless_http kwarg --- src/fastmcp/server/server.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index c424fa6e1..65d9b7d79 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -1342,6 +1342,7 @@ class FastMCP(Generic[LifespanResultT]): path: str | None = None, uvicorn_config: dict[str, Any] | None = None, middleware: list[ASGIMiddleware] | None = None, + stateless_http: bool | None = None, ) -> None: """Run the server using HTTP transport. @@ -1352,6 +1353,8 @@ class FastMCP(Generic[LifespanResultT]): log_level: Log level for the server (defaults to settings.log_level) path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path) uvicorn_config: Additional configuration for the Uvicorn server + middleware: A list of middleware to apply to the app + stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http) """ host = host or self._deprecated_settings.host port = port or self._deprecated_settings.port @@ -1359,7 +1362,12 @@ class FastMCP(Generic[LifespanResultT]): log_level or self._deprecated_settings.log_level ).lower() - app = self.http_app(path=path, transport=transport, middleware=middleware) + app = self.http_app( + path=path, + transport=transport, + middleware=middleware, + stateless_http=stateless_http, + ) _uvicorn_config_from_user = uvicorn_config or {} From e7fe4ff1a3dc7d0bd97ee266f15408eb7364f332 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:55:42 -0400 Subject: [PATCH 2/3] Remove second settings object; deprecate --- src/fastmcp/settings.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index 6219c1dc1..11b0a5f0f 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -1,6 +1,7 @@ from __future__ import annotations as _annotations import inspect +import warnings from pathlib import Path from typing import Annotated, Any, Literal @@ -258,4 +259,21 @@ class Settings(BaseSettings): ] = None -settings = Settings() +def __getattr__(name: str): + """ + Used to deprecate the module-level Image class; can be removed once it is no longer imported to root. + """ + if name == "settings": + import fastmcp + + settings = fastmcp.settings + # Deprecated in 2.10.2 + if settings.deprecation_warnings: + warnings.warn( + "`from fastmcp.settings import settings` is deprecated. use `fasmtpc.settings` instead.", + DeprecationWarning, + stacklevel=2, + ) + return settings + + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") From a52c28c319fd4455b30e9f7210746c20c7700f08 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:55:49 -0400 Subject: [PATCH 3/3] Ensure tests use stateless mode --- tests/client/test_streamable_http.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/client/test_streamable_http.py b/tests/client/test_streamable_http.py index f27f99399..4d5eaffef 100644 --- a/tests/client/test_streamable_http.py +++ b/tests/client/test_streamable_http.py @@ -2,7 +2,7 @@ import asyncio import json import sys from collections.abc import AsyncGenerator -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, call import pytest import uvicorn @@ -53,6 +53,7 @@ def fastmcp_server(): async def greet_with_progress(name: str, ctx: Context) -> str: """Report progress for a greeting.""" await ctx.report_progress(0.5, 1.0, "Greeting in progress") + await ctx.report_progress(0.75, 1.0, "Almost there!") return f"Hello, {name}!" # Add a resource @@ -108,8 +109,9 @@ def run_nested_server(host: str, port: int) -> None: @pytest.fixture() async def streamable_http_server( - stateless_http: bool = False, + request, ) -> AsyncGenerator[str, None]: + stateless_http = getattr(request, "param", False) with run_server_in_process( run_server, stateless_http=stateless_http, transport="http" ) as url: @@ -176,16 +178,25 @@ async def test_greet_with_progress_tool(streamable_http_server: str): result = await client.call_tool("greet_with_progress", {"name": "Alice"}) assert result.data == "Hello, Alice!" - progress_handler.assert_called_once_with(0.5, 1.0, "Greeting in progress") + progress_handler.assert_has_calls( + [ + call(0.5, 1.0, "Greeting in progress"), + call(0.75, 1.0, "Almost there!"), + ] + ) @pytest.mark.parametrize("streamable_http_server", [True, False], indirect=True) -async def test_elicitation_tool(streamable_http_server: str): +async def test_elicitation_tool(streamable_http_server: str, request): """Test calling the elicitation tool in both stateless and stateful modes.""" async def elicitation_handler(message, response_type, params, ctx): return {"value": "Alice"} + stateless_http = request.node.callspec.params.get("streamable_http_server", False) + if stateless_http: + pytest.xfail("Elicitation is not supported in stateless HTTP mode") + async with Client( transport=StreamableHttpTransport(streamable_http_server), elicitation_handler=elicitation_handler,