Skip to content

Commit 8e39915

Browse files
fix(streamable-http): downgrade stateless 'Terminating session: None' log
In stateless mode every request created a transport with mcp_session_id=None and terminated it on completion, producing 'INFO: Terminating session: None' on every request. The repeated noise made real session terminations hard to find and confused users into thinking their connection was dropping. Branch on mcp_session_id in terminate(): keep the existing INFO log for stateful session terminations, and switch the stateless path to a DEBUG log with a clearer message ("Stateless request completed, cleaning up transport"). Adds two caplog tests covering both branches. Closes #2329.
1 parent 19fe9fa commit 8e39915

2 files changed

Lines changed: 42 additions & 1 deletion

File tree

src/mcp/server/streamable_http.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,10 @@ async def terminate(self) -> None:
767767
"""
768768

769769
self._terminated = True
770-
logger.info(f"Terminating session: {self.mcp_session_id}")
770+
if self.mcp_session_id is not None:
771+
logger.info(f"Terminating session: {self.mcp_session_id}")
772+
else:
773+
logger.debug("Stateless request completed, cleaning up transport")
771774

772775
# We need a copy of the keys to avoid modification during iteration
773776
request_stream_keys = list(self._request_streams.keys())

tests/shared/test_streamable_http.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations as _annotations
88

99
import json
10+
import logging
1011
import time
1112
from collections.abc import AsyncIterator
1213
from contextlib import asynccontextmanager
@@ -588,6 +589,43 @@ def test_streamable_http_transport_init_validation() -> None:
588589
StreamableHTTPServerTransport(mcp_session_id="test\n")
589590

590591

592+
@pytest.mark.anyio
593+
async def test_terminate_stateless_log_is_debug(caplog: pytest.LogCaptureFixture):
594+
"""Stateless terminate() should not emit INFO 'Terminating session: None'.
595+
596+
Regression test for issue #2329: in stateless mode the transport has no
597+
session id, so the prior INFO log produced 'Terminating session: None' on
598+
every request. The stateless path now logs at DEBUG with a clearer message,
599+
while the stateful path keeps the INFO-level log.
600+
"""
601+
transport = StreamableHTTPServerTransport(mcp_session_id=None)
602+
603+
with caplog.at_level(logging.DEBUG, logger="mcp.server.streamable_http"):
604+
await transport.terminate()
605+
606+
info_records = [r for r in caplog.records if r.levelno == logging.INFO]
607+
assert not any("Terminating session" in r.getMessage() for r in info_records), (
608+
"Stateless terminate() must not emit INFO 'Terminating session: ...'"
609+
)
610+
assert any(
611+
r.levelno == logging.DEBUG and "Stateless request completed" in r.getMessage() for r in caplog.records
612+
), "Stateless terminate() should log a DEBUG completion message"
613+
614+
615+
@pytest.mark.anyio
616+
async def test_terminate_stateful_log_is_info(caplog: pytest.LogCaptureFixture):
617+
"""Stateful terminate() should still log session id at INFO (#2329)."""
618+
session_id = "abc123"
619+
transport = StreamableHTTPServerTransport(mcp_session_id=session_id)
620+
621+
with caplog.at_level(logging.INFO, logger="mcp.server.streamable_http"):
622+
await transport.terminate()
623+
624+
assert any(
625+
r.levelno == logging.INFO and f"Terminating session: {session_id}" in r.getMessage() for r in caplog.records
626+
), "Stateful terminate() must still emit INFO 'Terminating session: <id>'"
627+
628+
591629
@pytest.mark.anyio
592630
async def test_session_termination(basic_app: Starlette) -> None:
593631
"""DELETE terminates the session, after which requests for it return 404."""

0 commit comments

Comments
 (0)