Spaces:
Running
Running
Merge branch 'main' into mcpconfig-timeout
Browse files- src/fastmcp/server/server.py +9 -1
- src/fastmcp/settings.py +19 -1
- tests/client/test_streamable_http.py +15 -4
src/fastmcp/server/server.py
CHANGED
|
@@ -1370,6 +1370,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1370 |
path: str | None = None,
|
| 1371 |
uvicorn_config: dict[str, Any] | None = None,
|
| 1372 |
middleware: list[ASGIMiddleware] | None = None,
|
|
|
|
| 1373 |
) -> None:
|
| 1374 |
"""Run the server using HTTP transport.
|
| 1375 |
|
|
@@ -1380,6 +1381,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1380 |
log_level: Log level for the server (defaults to settings.log_level)
|
| 1381 |
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
| 1382 |
uvicorn_config: Additional configuration for the Uvicorn server
|
|
|
|
|
|
|
| 1383 |
"""
|
| 1384 |
|
| 1385 |
host = host or self._deprecated_settings.host
|
|
@@ -1388,7 +1391,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1388 |
log_level or self._deprecated_settings.log_level
|
| 1389 |
).lower()
|
| 1390 |
|
| 1391 |
-
app = self.http_app(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1392 |
|
| 1393 |
# Get the path for the server URL
|
| 1394 |
server_path = (
|
|
|
|
| 1370 |
path: str | None = None,
|
| 1371 |
uvicorn_config: dict[str, Any] | None = None,
|
| 1372 |
middleware: list[ASGIMiddleware] | None = None,
|
| 1373 |
+
stateless_http: bool | None = None,
|
| 1374 |
) -> None:
|
| 1375 |
"""Run the server using HTTP transport.
|
| 1376 |
|
|
|
|
| 1381 |
log_level: Log level for the server (defaults to settings.log_level)
|
| 1382 |
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
| 1383 |
uvicorn_config: Additional configuration for the Uvicorn server
|
| 1384 |
+
middleware: A list of middleware to apply to the app
|
| 1385 |
+
stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
|
| 1386 |
"""
|
| 1387 |
|
| 1388 |
host = host or self._deprecated_settings.host
|
|
|
|
| 1391 |
log_level or self._deprecated_settings.log_level
|
| 1392 |
).lower()
|
| 1393 |
|
| 1394 |
+
app = self.http_app(
|
| 1395 |
+
path=path,
|
| 1396 |
+
transport=transport,
|
| 1397 |
+
middleware=middleware,
|
| 1398 |
+
stateless_http=stateless_http,
|
| 1399 |
+
)
|
| 1400 |
|
| 1401 |
# Get the path for the server URL
|
| 1402 |
server_path = (
|
src/fastmcp/settings.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from __future__ import annotations as _annotations
|
| 2 |
|
| 3 |
import inspect
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
from typing import Annotated, Any, Literal
|
| 6 |
|
|
@@ -258,4 +259,21 @@ class Settings(BaseSettings):
|
|
| 258 |
] = None
|
| 259 |
|
| 260 |
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from __future__ import annotations as _annotations
|
| 2 |
|
| 3 |
import inspect
|
| 4 |
+
import warnings
|
| 5 |
from pathlib import Path
|
| 6 |
from typing import Annotated, Any, Literal
|
| 7 |
|
|
|
|
| 259 |
] = None
|
| 260 |
|
| 261 |
|
| 262 |
+
def __getattr__(name: str):
|
| 263 |
+
"""
|
| 264 |
+
Used to deprecate the module-level Image class; can be removed once it is no longer imported to root.
|
| 265 |
+
"""
|
| 266 |
+
if name == "settings":
|
| 267 |
+
import fastmcp
|
| 268 |
+
|
| 269 |
+
settings = fastmcp.settings
|
| 270 |
+
# Deprecated in 2.10.2
|
| 271 |
+
if settings.deprecation_warnings:
|
| 272 |
+
warnings.warn(
|
| 273 |
+
"`from fastmcp.settings import settings` is deprecated. use `fasmtpc.settings` instead.",
|
| 274 |
+
DeprecationWarning,
|
| 275 |
+
stacklevel=2,
|
| 276 |
+
)
|
| 277 |
+
return settings
|
| 278 |
+
|
| 279 |
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
tests/client/test_streamable_http.py
CHANGED
|
@@ -2,7 +2,7 @@ import asyncio
|
|
| 2 |
import json
|
| 3 |
import sys
|
| 4 |
from collections.abc import AsyncGenerator
|
| 5 |
-
from unittest.mock import AsyncMock
|
| 6 |
|
| 7 |
import pytest
|
| 8 |
import uvicorn
|
|
@@ -53,6 +53,7 @@ def fastmcp_server():
|
|
| 53 |
async def greet_with_progress(name: str, ctx: Context) -> str:
|
| 54 |
"""Report progress for a greeting."""
|
| 55 |
await ctx.report_progress(0.5, 1.0, "Greeting in progress")
|
|
|
|
| 56 |
return f"Hello, {name}!"
|
| 57 |
|
| 58 |
# Add a resource
|
|
@@ -108,8 +109,9 @@ def run_nested_server(host: str, port: int) -> None:
|
|
| 108 |
|
| 109 |
@pytest.fixture()
|
| 110 |
async def streamable_http_server(
|
| 111 |
-
|
| 112 |
) -> AsyncGenerator[str, None]:
|
|
|
|
| 113 |
with run_server_in_process(
|
| 114 |
run_server, stateless_http=stateless_http, transport="http"
|
| 115 |
) as url:
|
|
@@ -176,16 +178,25 @@ async def test_greet_with_progress_tool(streamable_http_server: str):
|
|
| 176 |
result = await client.call_tool("greet_with_progress", {"name": "Alice"})
|
| 177 |
assert result.data == "Hello, Alice!"
|
| 178 |
|
| 179 |
-
progress_handler.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
|
| 182 |
@pytest.mark.parametrize("streamable_http_server", [True, False], indirect=True)
|
| 183 |
-
async def test_elicitation_tool(streamable_http_server: str):
|
| 184 |
"""Test calling the elicitation tool in both stateless and stateful modes."""
|
| 185 |
|
| 186 |
async def elicitation_handler(message, response_type, params, ctx):
|
| 187 |
return {"value": "Alice"}
|
| 188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
async with Client(
|
| 190 |
transport=StreamableHttpTransport(streamable_http_server),
|
| 191 |
elicitation_handler=elicitation_handler,
|
|
|
|
| 2 |
import json
|
| 3 |
import sys
|
| 4 |
from collections.abc import AsyncGenerator
|
| 5 |
+
from unittest.mock import AsyncMock, call
|
| 6 |
|
| 7 |
import pytest
|
| 8 |
import uvicorn
|
|
|
|
| 53 |
async def greet_with_progress(name: str, ctx: Context) -> str:
|
| 54 |
"""Report progress for a greeting."""
|
| 55 |
await ctx.report_progress(0.5, 1.0, "Greeting in progress")
|
| 56 |
+
await ctx.report_progress(0.75, 1.0, "Almost there!")
|
| 57 |
return f"Hello, {name}!"
|
| 58 |
|
| 59 |
# Add a resource
|
|
|
|
| 109 |
|
| 110 |
@pytest.fixture()
|
| 111 |
async def streamable_http_server(
|
| 112 |
+
request,
|
| 113 |
) -> AsyncGenerator[str, None]:
|
| 114 |
+
stateless_http = getattr(request, "param", False)
|
| 115 |
with run_server_in_process(
|
| 116 |
run_server, stateless_http=stateless_http, transport="http"
|
| 117 |
) as url:
|
|
|
|
| 178 |
result = await client.call_tool("greet_with_progress", {"name": "Alice"})
|
| 179 |
assert result.data == "Hello, Alice!"
|
| 180 |
|
| 181 |
+
progress_handler.assert_has_calls(
|
| 182 |
+
[
|
| 183 |
+
call(0.5, 1.0, "Greeting in progress"),
|
| 184 |
+
call(0.75, 1.0, "Almost there!"),
|
| 185 |
+
]
|
| 186 |
+
)
|
| 187 |
|
| 188 |
|
| 189 |
@pytest.mark.parametrize("streamable_http_server", [True, False], indirect=True)
|
| 190 |
+
async def test_elicitation_tool(streamable_http_server: str, request):
|
| 191 |
"""Test calling the elicitation tool in both stateless and stateful modes."""
|
| 192 |
|
| 193 |
async def elicitation_handler(message, response_type, params, ctx):
|
| 194 |
return {"value": "Alice"}
|
| 195 |
|
| 196 |
+
stateless_http = request.node.callspec.params.get("streamable_http_server", False)
|
| 197 |
+
if stateless_http:
|
| 198 |
+
pytest.xfail("Elicitation is not supported in stateless HTTP mode")
|
| 199 |
+
|
| 200 |
async with Client(
|
| 201 |
transport=StreamableHttpTransport(streamable_http_server),
|
| 202 |
elicitation_handler=elicitation_handler,
|