Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
58bd353
1
Parent(s): f827f24
test nested servers
Browse files- pyproject.toml +1 -1
- src/fastmcp/server/http.py +18 -14
- tests/client/test_streamable_http.py +39 -0
pyproject.toml
CHANGED
|
@@ -83,7 +83,7 @@ asyncio_mode = "auto"
|
|
| 83 |
asyncio_default_fixture_loop_scope = "session"
|
| 84 |
asyncio_default_test_loop_scope = "session"
|
| 85 |
filterwarnings = []
|
| 86 |
-
timeout =
|
| 87 |
|
| 88 |
[tool.pyright]
|
| 89 |
include = ["src", "tests"]
|
|
|
|
| 83 |
asyncio_default_fixture_loop_scope = "session"
|
| 84 |
asyncio_default_test_loop_scope = "session"
|
| 85 |
filterwarnings = []
|
| 86 |
+
timeout = 3
|
| 87 |
|
| 88 |
[tool.pyright]
|
| 89 |
include = ["src", "tests"]
|
src/fastmcp/server/http.py
CHANGED
|
@@ -110,7 +110,7 @@ def setup_auth_middleware_and_routes(
|
|
| 110 |
def create_base_app(
|
| 111 |
routes: list[Route | Mount],
|
| 112 |
middleware: list[Middleware],
|
| 113 |
-
debug: bool,
|
| 114 |
lifespan: Callable | None = None,
|
| 115 |
) -> Starlette:
|
| 116 |
"""Create a base Starlette app with common middleware and routes.
|
|
@@ -127,17 +127,12 @@ def create_base_app(
|
|
| 127 |
# Always add RequestContextMiddleware as the outermost middleware
|
| 128 |
middleware.append(Middleware(RequestContextMiddleware))
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
if lifespan:
|
| 138 |
-
app_kwargs["lifespan"] = lifespan
|
| 139 |
-
|
| 140 |
-
return Starlette(**app_kwargs)
|
| 141 |
|
| 142 |
|
| 143 |
def create_sse_app(
|
|
@@ -224,7 +219,11 @@ def create_sse_app(
|
|
| 224 |
routes.extend(cast(list[Route | Mount], additional_routes))
|
| 225 |
|
| 226 |
# Create and return the app
|
| 227 |
-
return create_base_app(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
|
| 230 |
def create_streamable_http_app(
|
|
@@ -305,4 +304,9 @@ def create_streamable_http_app(
|
|
| 305 |
yield
|
| 306 |
|
| 307 |
# Create and return the app with lifespan
|
| 308 |
-
return create_base_app(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
def create_base_app(
|
| 111 |
routes: list[Route | Mount],
|
| 112 |
middleware: list[Middleware],
|
| 113 |
+
debug: bool = False,
|
| 114 |
lifespan: Callable | None = None,
|
| 115 |
) -> Starlette:
|
| 116 |
"""Create a base Starlette app with common middleware and routes.
|
|
|
|
| 127 |
# Always add RequestContextMiddleware as the outermost middleware
|
| 128 |
middleware.append(Middleware(RequestContextMiddleware))
|
| 129 |
|
| 130 |
+
return Starlette(
|
| 131 |
+
routes=routes,
|
| 132 |
+
middleware=middleware,
|
| 133 |
+
debug=debug,
|
| 134 |
+
lifespan=lifespan,
|
| 135 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
def create_sse_app(
|
|
|
|
| 219 |
routes.extend(cast(list[Route | Mount], additional_routes))
|
| 220 |
|
| 221 |
# Create and return the app
|
| 222 |
+
return create_base_app(
|
| 223 |
+
routes=routes,
|
| 224 |
+
middleware=middleware,
|
| 225 |
+
debug=debug,
|
| 226 |
+
)
|
| 227 |
|
| 228 |
|
| 229 |
def create_streamable_http_app(
|
|
|
|
| 304 |
yield
|
| 305 |
|
| 306 |
# Create and return the app with lifespan
|
| 307 |
+
return create_base_app(
|
| 308 |
+
routes=routes,
|
| 309 |
+
middleware=middleware,
|
| 310 |
+
debug=debug,
|
| 311 |
+
lifespan=lifespan,
|
| 312 |
+
)
|
tests/client/test_streamable_http.py
CHANGED
|
@@ -5,6 +5,8 @@ from collections.abc import Generator
|
|
| 5 |
import pytest
|
| 6 |
import uvicorn
|
| 7 |
from mcp.types import TextResourceContents
|
|
|
|
|
|
|
| 8 |
|
| 9 |
from fastmcp.client import Client
|
| 10 |
from fastmcp.client.transports import StreamableHttpTransport
|
|
@@ -100,3 +102,40 @@ async def test_http_headers(streamable_http_server: str):
|
|
| 100 |
json_result = json.loads(raw_result[0].text)
|
| 101 |
assert "x-demo-header" in json_result
|
| 102 |
assert json_result["x-demo-header"] == "ABC"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import pytest
|
| 6 |
import uvicorn
|
| 7 |
from mcp.types import TextResourceContents
|
| 8 |
+
from starlette.applications import Starlette
|
| 9 |
+
from starlette.routing import Mount
|
| 10 |
|
| 11 |
from fastmcp.client import Client
|
| 12 |
from fastmcp.client.transports import StreamableHttpTransport
|
|
|
|
| 102 |
json_result = json.loads(raw_result[0].text)
|
| 103 |
assert "x-demo-header" in json_result
|
| 104 |
assert json_result["x-demo-header"] == "ABC"
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def run_nested_server(host: str, port: int) -> None:
|
| 108 |
+
try:
|
| 109 |
+
mcp_app = fastmcp_server().streamable_http_app()
|
| 110 |
+
|
| 111 |
+
mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)])
|
| 112 |
+
mount2 = Starlette(
|
| 113 |
+
routes=[Mount("/nest-outer", app=mount)],
|
| 114 |
+
lifespan=mcp_app.router.lifespan_context,
|
| 115 |
+
)
|
| 116 |
+
server = uvicorn.Server(
|
| 117 |
+
config=uvicorn.Config(
|
| 118 |
+
app=mount2,
|
| 119 |
+
host=host,
|
| 120 |
+
port=port,
|
| 121 |
+
log_level="error",
|
| 122 |
+
lifespan="on",
|
| 123 |
+
)
|
| 124 |
+
)
|
| 125 |
+
server.run()
|
| 126 |
+
except Exception as e:
|
| 127 |
+
print(f"Server error: {e}")
|
| 128 |
+
sys.exit(1)
|
| 129 |
+
sys.exit(0)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
async def test_nested_streamable_http_server_resolves_correctly():
|
| 133 |
+
# tests patch for
|
| 134 |
+
# https://github.com/modelcontextprotocol/python-sdk/pull/659
|
| 135 |
+
|
| 136 |
+
with run_server_in_process(run_nested_server) as url:
|
| 137 |
+
async with Client(
|
| 138 |
+
transport=StreamableHttpTransport(f"{url}/nest-outer/nest-inner/mcp")
|
| 139 |
+
) as client:
|
| 140 |
+
result = await client.ping()
|
| 141 |
+
assert result is True
|