Update routing for streamable HTTP to avoid 307 redirect (#1115)
Co-authored-by: ihrpr <inna@anthropic.com>
This commit is contained in:
@@ -828,7 +828,6 @@ class FastMCP:
|
||||
def streamable_http_app(self) -> Starlette:
|
||||
"""Return an instance of the StreamableHTTP server app."""
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.routing import Mount
|
||||
|
||||
# Create session manager on first call (lazy initialization)
|
||||
if self._session_manager is None:
|
||||
@@ -841,8 +840,7 @@ class FastMCP:
|
||||
)
|
||||
|
||||
# Create the ASGI handler
|
||||
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
|
||||
await self.session_manager.handle_request(scope, receive, send)
|
||||
streamable_http_app = StreamableHTTPASGIApp(self._session_manager)
|
||||
|
||||
# Create routes
|
||||
routes: list[Route | Mount] = []
|
||||
@@ -889,17 +887,17 @@ class FastMCP:
|
||||
)
|
||||
|
||||
routes.append(
|
||||
Mount(
|
||||
Route(
|
||||
self.settings.streamable_http_path,
|
||||
app=RequireAuthMiddleware(handle_streamable_http, required_scopes, resource_metadata_url),
|
||||
endpoint=RequireAuthMiddleware(streamable_http_app, required_scopes, resource_metadata_url),
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Auth is disabled, no wrapper needed
|
||||
routes.append(
|
||||
Mount(
|
||||
Route(
|
||||
self.settings.streamable_http_path,
|
||||
app=handle_streamable_http,
|
||||
endpoint=streamable_http_app,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -972,6 +970,18 @@ class FastMCP:
|
||||
raise ValueError(str(e))
|
||||
|
||||
|
||||
class StreamableHTTPASGIApp:
|
||||
"""
|
||||
ASGI application for Streamable HTTP server transport.
|
||||
"""
|
||||
|
||||
def __init__(self, session_manager: StreamableHTTPSessionManager):
|
||||
self.session_manager = session_manager
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
await self.session_manager.handle_request(scope, receive, send)
|
||||
|
||||
|
||||
class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
|
||||
"""Context object providing access to MCP capabilities.
|
||||
|
||||
|
||||
@@ -1072,3 +1072,22 @@ class TestServerPrompts:
|
||||
async with client_session(mcp._mcp_server) as client:
|
||||
with pytest.raises(McpError, match="Missing required arguments"):
|
||||
await client.get_prompt("prompt_fn")
|
||||
|
||||
|
||||
def test_streamable_http_no_redirect() -> None:
|
||||
"""Test that streamable HTTP routes are correctly configured."""
|
||||
mcp = FastMCP()
|
||||
app = mcp.streamable_http_app()
|
||||
|
||||
# Find routes by type - streamable_http_app creates Route objects, not Mount objects
|
||||
streamable_routes = [
|
||||
r
|
||||
for r in app.routes
|
||||
if isinstance(r, Route) and hasattr(r, "path") and r.path == mcp.settings.streamable_http_path
|
||||
]
|
||||
|
||||
# Verify routes exist
|
||||
assert len(streamable_routes) == 1, "Should have one streamable route"
|
||||
|
||||
# Verify path values
|
||||
assert streamable_routes[0].path == "/mcp", "Streamable route path should be /mcp"
|
||||
|
||||
Reference in New Issue
Block a user