Skip to content

Commit 1e77a6d

Browse files
committed
Add SseServerTransport.head_response so the transport answers HEAD
1 parent 58bff01 commit 1e77a6d

2 files changed

Lines changed: 28 additions & 19 deletions

File tree

‎src/mcp/server/mcpserver/server.py‎

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,7 @@
9898
StreamableHTTPSessionManager,
9999
)
100100
from mcp.server.subscriptions import InMemorySubscriptionBus, ListenHandler, SubscriptionBus
101-
from mcp.server.transport_security import (
102-
DEFAULT_MAX_REQUEST_BODY_SIZE,
103-
TransportSecurityMiddleware,
104-
TransportSecuritySettings,
105-
)
101+
from mcp.server.transport_security import DEFAULT_MAX_REQUEST_BODY_SIZE, TransportSecuritySettings
106102
from mcp.shared.exceptions import MCPError
107103
from mcp.shared.uri_template import UriTemplate
108104

@@ -1175,18 +1171,9 @@ def sse_app(
11751171
message_path, security_settings=transport_security, max_request_body_size=max_request_body_size
11761172
)
11771173

1178-
security = TransportSecurityMiddleware(transport_security)
1179-
11801174
async def sse_endpoint(request: Request) -> Response:
11811175
if request.method == "HEAD":
1182-
if error_response := await security.validate_request(request):
1183-
return error_response
1184-
# GET's headers without its never-ending stream; a Content-Length would claim an empty one.
1185-
response = Response(
1186-
media_type="text/event-stream", headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"}
1187-
)
1188-
del response.headers["content-length"]
1189-
return response
1176+
return await sse.head_response(request)
11901177
async with sse.connect_sse( # pragma: no cover
11911178
request.scope,
11921179
request.receive,

‎src/mcp/server/sse.py‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@
1515
1616
# Define handler functions
1717
async def handle_sse(request):
18-
# Starlette also routes HEAD here: answer with GET's headers, not its never-ending stream
18+
# Starlette also routes HEAD here, and the stream would never end
1919
if request.method == "HEAD":
20-
response = Response(media_type="text/event-stream", headers={"Cache-Control": "no-store"})
21-
del response.headers["content-length"]
22-
return response
20+
return await sse.head_response(request)
2321
async with sse.connect_sse(
2422
request.scope, request.receive, request._send
2523
) as streams:
@@ -77,6 +75,9 @@ class SseServerTransport:
7775
2. handle_post_message() is an ASGI application which receives incoming POST
7876
requests, which should contain client messages that link to a
7977
previously-established SSE session.
78+
79+
head_response() answers a HEAD request to the SSE endpoint, which Starlette
80+
routes to the same endpoint as GET.
8081
"""
8182

8283
_endpoint: str
@@ -139,6 +140,27 @@ def __init__(
139140
self._post_message_app = RequestBodyLimitMiddleware(self._handle_post_message, max_request_body_size)
140141
logger.debug(f"SseServerTransport initialized with endpoint: {endpoint}")
141142

143+
async def head_response(self, request: Request) -> Response:
144+
"""Build the response to a HEAD request on the SSE endpoint.
145+
146+
An SSE stream never ends, so a HEAD served through `connect_sse()` would never complete.
147+
This runs the same Host/Origin validation and returns the headers a GET gets, with no body.
148+
149+
Args:
150+
request: The HEAD request.
151+
152+
Returns:
153+
The validation error response, or the SSE headers with no body.
154+
"""
155+
if error_response := await self._security.validate_request(request, is_post=False):
156+
return error_response
157+
response = Response(
158+
media_type="text/event-stream", headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"}
159+
)
160+
# A Content-Length would claim an empty stream.
161+
del response.headers["content-length"]
162+
return response
163+
142164
@asynccontextmanager
143165
async def connect_sse(self, scope: Scope, receive: Receive, send: Send):
144166
if scope["type"] != "http":

0 commit comments

Comments
 (0)