Skip to content

Commit fd11eca

Browse files
yurikunashihrpr
andauthored
Update routing for streamable HTTP to avoid 307 redirect (#1115)
Co-authored-by: ihrpr <inna@anthropic.com>
1 parent 0a4e8d4 commit fd11eca

File tree

2 files changed

+36
-7
lines changed

2 files changed

+36
-7
lines changed

src/mcp/server/fastmcp/server.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,6 @@ async def sse_endpoint(request: Request) -> Response:
828828
def streamable_http_app(self) -> Starlette:
829829
"""Return an instance of the StreamableHTTP server app."""
830830
from starlette.middleware import Middleware
831-
from starlette.routing import Mount
832831

833832
# Create session manager on first call (lazy initialization)
834833
if self._session_manager is None:
@@ -841,8 +840,7 @@ def streamable_http_app(self) -> Starlette:
841840
)
842841

843842
# Create the ASGI handler
844-
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
845-
await self.session_manager.handle_request(scope, receive, send)
843+
streamable_http_app = StreamableHTTPASGIApp(self._session_manager)
846844

847845
# Create routes
848846
routes: list[Route | Mount] = []
@@ -889,17 +887,17 @@ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) ->
889887
)
890888

891889
routes.append(
892-
Mount(
890+
Route(
893891
self.settings.streamable_http_path,
894-
app=RequireAuthMiddleware(handle_streamable_http, required_scopes, resource_metadata_url),
892+
endpoint=RequireAuthMiddleware(streamable_http_app, required_scopes, resource_metadata_url),
895893
)
896894
)
897895
else:
898896
# Auth is disabled, no wrapper needed
899897
routes.append(
900-
Mount(
898+
Route(
901899
self.settings.streamable_http_path,
902-
app=handle_streamable_http,
900+
endpoint=streamable_http_app,
903901
)
904902
)
905903

@@ -972,6 +970,18 @@ async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -
972970
raise ValueError(str(e))
973971

974972

973+
class StreamableHTTPASGIApp:
974+
"""
975+
ASGI application for Streamable HTTP server transport.
976+
"""
977+
978+
def __init__(self, session_manager: StreamableHTTPSessionManager):
979+
self.session_manager = session_manager
980+
981+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
982+
await self.session_manager.handle_request(scope, receive, send)
983+
984+
975985
class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
976986
"""Context object providing access to MCP capabilities.
977987

tests/server/fastmcp/test_server.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,3 +1072,22 @@ def prompt_fn(name: str) -> str:
10721072
async with client_session(mcp._mcp_server) as client:
10731073
with pytest.raises(McpError, match="Missing required arguments"):
10741074
await client.get_prompt("prompt_fn")
1075+
1076+
1077+
def test_streamable_http_no_redirect() -> None:
1078+
"""Test that streamable HTTP routes are correctly configured."""
1079+
mcp = FastMCP()
1080+
app = mcp.streamable_http_app()
1081+
1082+
# Find routes by type - streamable_http_app creates Route objects, not Mount objects
1083+
streamable_routes = [
1084+
r
1085+
for r in app.routes
1086+
if isinstance(r, Route) and hasattr(r, "path") and r.path == mcp.settings.streamable_http_path
1087+
]
1088+
1089+
# Verify routes exist
1090+
assert len(streamable_routes) == 1, "Should have one streamable route"
1091+
1092+
# Verify path values
1093+
assert streamable_routes[0].path == "/mcp", "Streamable route path should be /mcp"

0 commit comments

Comments
 (0)