diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index f14201857c..c46851c2b1 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -769,7 +769,10 @@ async def terminate(self) -> None: """ self._terminated = True - logger.info(f"Terminating session: {self.mcp_session_id}") + if self.mcp_session_id is not None: + logger.info(f"Terminating session: {self.mcp_session_id}") + else: + logger.debug("Stateless request completed, cleaning up transport") # We need a copy of the keys to avoid modification during iteration request_stream_keys = list(self._request_streams.keys()) diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 3d5770fb61..93fbc801b9 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -6,6 +6,7 @@ from __future__ import annotations as _annotations import json +import logging import multiprocessing import socket import time @@ -767,6 +768,43 @@ def test_streamable_http_transport_init_validation(): StreamableHTTPServerTransport(mcp_session_id="test\n") +@pytest.mark.anyio +async def test_terminate_stateless_log_is_debug(caplog: pytest.LogCaptureFixture): + """Stateless terminate() should not emit INFO 'Terminating session: None'. + + Regression test for issue #2329: in stateless mode the transport has no + session id, so the prior INFO log produced 'Terminating session: None' on + every request. The stateless path now logs at DEBUG with a clearer message, + while the stateful path keeps the INFO-level log. + """ + transport = StreamableHTTPServerTransport(mcp_session_id=None) + + with caplog.at_level(logging.DEBUG, logger="mcp.server.streamable_http"): + await transport.terminate() + + info_records = [r for r in caplog.records if r.levelno == logging.INFO] + assert not any("Terminating session" in r.getMessage() for r in info_records), ( + "Stateless terminate() must not emit INFO 'Terminating session: ...'" + ) + assert any( + r.levelno == logging.DEBUG and "Stateless request completed" in r.getMessage() for r in caplog.records + ), "Stateless terminate() should log a DEBUG completion message" + + +@pytest.mark.anyio +async def test_terminate_stateful_log_is_info(caplog: pytest.LogCaptureFixture): + """Stateful terminate() should still log session id at INFO (#2329).""" + session_id = "abc123" + transport = StreamableHTTPServerTransport(mcp_session_id=session_id) + + with caplog.at_level(logging.INFO, logger="mcp.server.streamable_http"): + await transport.terminate() + + assert any( + r.levelno == logging.INFO and f"Terminating session: {session_id}" in r.getMessage() for r in caplog.records + ), "Stateful terminate() must still emit INFO 'Terminating session: '" + + def test_session_termination(basic_server: None, basic_server_url: str): """Test session termination via DELETE and subsequent request handling.""" response = requests.post(