Skip to content

Commit ec245fd

Browse files
Apply review feedback
- Add Content-Type headers to test scopes to fix CI - Add public is_terminated property to avoid accessing private _terminated - Remove unnecessary try-catch from test - Change logging from warning to error for session crashes
1 parent 8359301 commit ec245fd

File tree

3 files changed

+33
-28
lines changed

3 files changed

+33
-28
lines changed

src/mcp/server/streamable_http.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ def __init__(
173173
] = {}
174174
self._terminated = False
175175

176+
@property
177+
def is_terminated(self) -> bool:
178+
"""Check if this transport has been explicitly terminated."""
179+
return self._terminated
180+
176181
def _create_error_response(
177182
self,
178183
error_message: str,
@@ -452,7 +457,7 @@ async def sse_writer():
452457
):
453458
break
454459
except Exception as e:
455-
logger.warning(f"Error in SSE writer: {e}", exc_info=True)
460+
logger.exception(f"Error in SSE writer: {e}")
456461
finally:
457462
logger.debug("Closing SSE writer")
458463
await self._clean_up_memory_streams(request_id)
@@ -482,13 +487,13 @@ async def sse_writer():
482487
session_message = SessionMessage(message, metadata=metadata)
483488
await writer.send(session_message)
484489
except Exception:
485-
logger.warning("SSE response error", exc_info=True)
490+
logger.exception("SSE response error")
486491
await sse_stream_writer.aclose()
487492
await sse_stream_reader.aclose()
488493
await self._clean_up_memory_streams(request_id)
489494

490495
except Exception as err:
491-
logger.warning("Error handling POST request", exc_info=True)
496+
logger.exception("Error handling POST request")
492497
response = self._create_error_response(
493498
f"Error handling POST request: {err}",
494499
HTTPStatus.INTERNAL_SERVER_ERROR,
@@ -570,7 +575,7 @@ async def standalone_sse_writer():
570575
event_data = self._create_event_data(event_message)
571576
await sse_stream_writer.send(event_data)
572577
except Exception as e:
573-
logger.warning(f"Error in standalone SSE writer: {e}", exc_info=True)
578+
logger.exception(f"Error in standalone SSE writer: {e}")
574579
finally:
575580
logger.debug("Closing standalone SSE writer")
576581
await self._clean_up_memory_streams(GET_STREAM_KEY)
@@ -586,7 +591,7 @@ async def standalone_sse_writer():
586591
# This will send headers immediately and establish the SSE connection
587592
await response(request.scope, request.receive, send)
588593
except Exception as e:
589-
logger.warning(f"Error in standalone SSE response: {e}", exc_info=True)
594+
logger.exception(f"Error in standalone SSE response: {e}")
590595
await sse_stream_writer.aclose()
591596
await sse_stream_reader.aclose()
592597
await self._clean_up_memory_streams(GET_STREAM_KEY)

src/mcp/server/streamable_http_manager.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STA
180180
stateless=True,
181181
)
182182
except Exception as e:
183-
logger.warning(f"Stateless session crashed: {e}", exc_info=True)
183+
logger.error(f"Stateless session crashed: {e}", exc_info=True)
184184

185185
# Assert task group is not None for type checking
186186
assert self._task_group is not None
@@ -243,7 +243,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
243243
stateless=False, # Stateful mode
244244
)
245245
except Exception as e:
246-
logger.warning(
246+
logger.error(
247247
f"Session {http_transport.mcp_session_id} crashed: {e}",
248248
exc_info=True,
249249
)
@@ -252,9 +252,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
252252
if (
253253
http_transport.mcp_session_id
254254
and http_transport.mcp_session_id in self._server_instances
255-
and not (
256-
hasattr(http_transport, "_terminated") and http_transport._terminated # pyright: ignore
257-
)
255+
and not http_transport.is_terminated
258256
):
259257
logger.info(
260258
"Cleaning up crashed session "

tests/server/test_streamable_http_manager.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,12 @@ async def test_stateful_session_cleanup_on_graceful_exit(running_manager):
104104
async def mock_send(message):
105105
sent_messages.append(message)
106106

107-
scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []}
107+
scope = {
108+
"type": "http",
109+
"method": "POST",
110+
"path": "/mcp",
111+
"headers": [(b"content-type", b"application/json")],
112+
}
108113

109114
async def mock_receive():
110115
return {"type": "http.request", "body": b"", "more_body": False}
@@ -135,9 +140,9 @@ async def mock_receive():
135140
# Give other tasks a chance to run. This is important for the finally block.
136141
await anyio.sleep(0.01)
137142

138-
assert (
139-
session_id not in manager._server_instances
140-
), "Session ID should be removed from _server_instances after graceful exit"
143+
assert session_id not in manager._server_instances, (
144+
"Session ID should be removed from _server_instances after graceful exit"
145+
)
141146
assert not manager._server_instances, "No sessions should be tracked after the only session exits gracefully"
142147

143148

@@ -158,21 +163,18 @@ async def mock_send(message):
158163
if message["type"] == "http.response.start" and message["status"] >= 500:
159164
pass # Expected if TestException propagates that far up the transport
160165

161-
scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []}
166+
scope = {
167+
"type": "http",
168+
"method": "POST",
169+
"path": "/mcp",
170+
"headers": [(b"content-type", b"application/json")],
171+
}
162172

163173
async def mock_receive():
164174
return {"type": "http.request", "body": b"", "more_body": False}
165175

166-
# It's possible handle_request itself might raise an error if the TestException
167-
# isn't caught by the transport layer before propagating.
168-
# The key is that the session manager's internal task for MCPServer.run
169-
# encounters the exception.
170-
try:
171-
await manager.handle_request(scope, mock_receive, mock_send)
172-
except TestException:
173-
# This might be caught here if not handled by StreamableHTTPServerTransport's
174-
# error handling
175-
pass
176+
# Trigger session creation
177+
await manager.handle_request(scope, mock_receive, mock_send)
176178

177179
session_id = None
178180
for msg in sent_messages:
@@ -191,7 +193,7 @@ async def mock_receive():
191193
# Give other tasks a chance to run to ensure the finally block executes
192194
await anyio.sleep(0.01)
193195

194-
assert (
195-
session_id not in manager._server_instances
196-
), "Session ID should be removed from _server_instances after an exception"
196+
assert session_id not in manager._server_instances, (
197+
"Session ID should be removed from _server_instances after an exception"
198+
)
197199
assert not manager._server_instances, "No sessions should be tracked after the only session crashes"

0 commit comments

Comments
 (0)