From a0939e57fed62d6860fb2d2791a477ab2ff7c870 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 9 May 2025 17:27:22 -0400 Subject: [PATCH] test nested servers --- pyproject.toml | 2 +- src/fastmcp/server/http.py | 32 +++++++++++++---------- tests/client/test_streamable_http.py | 39 ++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dea5d446f..ce86e2df7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" asyncio_default_test_loop_scope = "session" filterwarnings = [] -timeout = 5 +timeout = 3 [tool.pyright] include = ["src", "tests"] diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index 547399559..a6996bdeb 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -110,7 +110,7 @@ def setup_auth_middleware_and_routes( def create_base_app( routes: list[Route | Mount], middleware: list[Middleware], - debug: bool, + debug: bool = False, lifespan: Callable | None = None, ) -> Starlette: """Create a base Starlette app with common middleware and routes. @@ -127,17 +127,12 @@ def create_base_app( # Always add RequestContextMiddleware as the outermost middleware middleware.append(Middleware(RequestContextMiddleware)) - # Create the app - app_kwargs = { - "debug": debug, - "routes": routes, - "middleware": middleware, - } - - if lifespan: - app_kwargs["lifespan"] = lifespan - - return Starlette(**app_kwargs) + return Starlette( + routes=routes, + middleware=middleware, + debug=debug, + lifespan=lifespan, + ) def create_sse_app( @@ -224,7 +219,11 @@ def create_sse_app( routes.extend(cast(list[Route | Mount], additional_routes)) # Create and return the app - return create_base_app(routes, middleware, debug) + return create_base_app( + routes=routes, + middleware=middleware, + debug=debug, + ) def create_streamable_http_app( @@ -305,4 +304,9 @@ def create_streamable_http_app( yield # Create and return the app with lifespan - return create_base_app(routes, middleware, debug, lifespan) + return create_base_app( + routes=routes, + middleware=middleware, + debug=debug, + lifespan=lifespan, + ) diff --git a/tests/client/test_streamable_http.py b/tests/client/test_streamable_http.py index 18455c3ff..117f783f0 100644 --- a/tests/client/test_streamable_http.py +++ b/tests/client/test_streamable_http.py @@ -5,6 +5,8 @@ from collections.abc import Generator import pytest import uvicorn from mcp.types import TextResourceContents +from starlette.applications import Starlette +from starlette.routing import Mount from fastmcp.client import Client from fastmcp.client.transports import StreamableHttpTransport @@ -100,3 +102,40 @@ async def test_http_headers(streamable_http_server: str): json_result = json.loads(raw_result[0].text) assert "x-demo-header" in json_result assert json_result["x-demo-header"] == "ABC" + + +def run_nested_server(host: str, port: int) -> None: + try: + mcp_app = fastmcp_server().streamable_http_app() + + mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)]) + mount2 = Starlette( + routes=[Mount("/nest-outer", app=mount)], + lifespan=mcp_app.router.lifespan_context, + ) + server = uvicorn.Server( + config=uvicorn.Config( + app=mount2, + host=host, + port=port, + log_level="error", + lifespan="on", + ) + ) + server.run() + except Exception as e: + print(f"Server error: {e}") + sys.exit(1) + sys.exit(0) + + +async def test_nested_streamable_http_server_resolves_correctly(): + # tests patch for + # https://github.com/modelcontextprotocol/python-sdk/pull/659 + + with run_server_in_process(run_nested_server) as url: + async with Client( + transport=StreamableHttpTransport(f"{url}/nest-outer/nest-inner/mcp") + ) as client: + result = await client.ping() + assert result is True