5151from starlette .middleware .authentication import AuthenticationMiddleware
5252from starlette .requests import Request
5353from starlette .responses import Response
54- from starlette .routing import Mount , Route , request_response
54+ from starlette .routing import Mount , Route
55+ from starlette .types import ASGIApp , Receive , Scope , Send
5556
5657from mcp .server .auth .middleware .auth_context import AuthContextMiddleware
5758from mcp .server .auth .middleware .bearer_auth import BearerAuthBackend , RequireAuthMiddleware
9899 StreamableHTTPSessionManager ,
99100)
100101from mcp .server .subscriptions import InMemorySubscriptionBus , ListenHandler , SubscriptionBus
101- from mcp .server .transport_security import DEFAULT_MAX_REQUEST_BODY_SIZE , TransportSecuritySettings
102+ from mcp .server .transport_security import (
103+ DEFAULT_MAX_REQUEST_BODY_SIZE ,
104+ TransportSecurityMiddleware ,
105+ TransportSecuritySettings ,
106+ )
102107from mcp .shared .exceptions import MCPError
103108from mcp .shared .uri_template import UriTemplate
104109
@@ -1171,23 +1176,23 @@ def sse_app(
11711176 message_path , security_settings = transport_security , max_request_body_size = max_request_body_size
11721177 )
11731178
1174- async def sse_endpoint (request : Request ) -> Response :
1175- if request .method == "HEAD" :
1176- # GET's headers without its never-ending stream; a Content-Length would claim an empty one.
1177- response = Response (
1178- media_type = "text/event-stream" , headers = {"Cache-Control" : "no-store" , "X-Accel-Buffering" : "no" }
1179- )
1180- del response .headers ["content-length" ]
1181- return response
1182- async with sse .connect_sse ( # pragma: no cover
1183- request .scope ,
1184- request .receive ,
1185- request ._send , # type: ignore[reportPrivateUsage]
1186- ) as streams :
1179+ security = TransportSecurityMiddleware (transport_security )
1180+
1181+ async def handle_sse (scope : Scope , receive : Receive , send : Send ) -> None :
1182+ if scope ["method" ] == "HEAD" :
1183+ response = await security .validate_request (Request (scope , receive ))
1184+ if response is None :
1185+ # GET's headers without its never-ending stream; a Content-Length would claim an empty one.
1186+ response = Response (
1187+ media_type = "text/event-stream" , headers = {"Cache-Control" : "no-store" , "X-Accel-Buffering" : "no" }
1188+ )
1189+ del response .headers ["content-length" ]
1190+ await response (scope , receive , send )
1191+ return
1192+ async with sse .connect_sse (scope , receive , send ) as streams : # pragma: no cover
11871193 await self ._lowlevel_server .run (
11881194 streams [0 ], streams [1 ], self ._lowlevel_server .create_initialization_options ()
11891195 )
1190- return Response () # pragma: no cover
11911196
11921197 # Create routes
11931198 routes : list [Route | Mount ] = []
@@ -1245,9 +1250,7 @@ async def sse_endpoint(request: Request) -> Response:
12451250 routes .append (
12461251 Route (
12471252 sse_path ,
1248- endpoint = RequireAuthMiddleware (
1249- request_response (sse_endpoint ), required_scopes , resource_metadata_url
1250- ),
1253+ endpoint = RequireAuthMiddleware (handle_sse , required_scopes , resource_metadata_url ),
12511254 methods = ["GET" ],
12521255 )
12531256 )
@@ -1262,7 +1265,7 @@ async def sse_endpoint(request: Request) -> Response:
12621265 routes .append (
12631266 Route (
12641267 sse_path ,
1265- endpoint = sse_endpoint ,
1268+ endpoint = _ASGIEndpoint ( handle_sse ) ,
12661269 methods = ["GET" ],
12671270 )
12681271 )
@@ -1426,3 +1429,14 @@ def require_client_extension(ctx: ServerRequestContext[Any, Any], identifier: st
14261429 message = f"Client did not declare required extension { identifier !r} " ,
14271430 data = data .model_dump (by_alias = True , mode = "json" , exclude_none = True ),
14281431 )
1432+
1433+
1434+ class _ASGIEndpoint :
1435+ """Hands a `Route` a raw ASGI app; a plain function would be wrapped as a request/response endpoint,
1436+ which sends a second response after the SSE stream has already completed."""
1437+
1438+ def __init__ (self , app : ASGIApp ) -> None :
1439+ self .app = app
1440+
1441+ async def __call__ (self , scope : Scope , receive : Receive , send : Send ) -> None :
1442+ await self .app (scope , receive , send )
0 commit comments