Skip to content

Commit 2c9e60c

Browse files
Remove _effective_idle_timeout, use session_idle_timeout directly
The helper silently clamped the user's configured timeout to retry_interval * 3, which is surprising. Users should set a timeout that suits their deployment. Updated the docstring to note that the timeout should comfortably exceed retry_interval when both are set.
1 parent 7345bd7 commit 2c9e60c

File tree

2 files changed

+8
-45
lines changed

2 files changed

+8
-45
lines changed

src/mcp/server/streamable_http_manager.py

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,11 @@ class StreamableHTTPSessionManager:
5959
session_idle_timeout: Optional idle timeout in seconds for stateful sessions.
6060
If set, sessions that receive no HTTP requests for this
6161
duration will be automatically terminated and removed.
62-
When retry_interval is also set, the effective idle
63-
threshold is at least ``retry_interval / 1000 * 3`` to
64-
avoid prematurely reaping sessions that are simply
65-
waiting for SSE polling reconnections. Default is None
66-
(no timeout). A value of 1800 (30 minutes) is
67-
recommended for most deployments.
62+
When retry_interval is also configured, ensure the idle
63+
timeout comfortably exceeds the retry interval to avoid
64+
reaping sessions during normal SSE polling gaps.
65+
Default is None (no timeout). A value of 1800
66+
(30 minutes) is recommended for most deployments.
6867
"""
6968

7069
def __init__(
@@ -237,8 +236,8 @@ async def _handle_stateful_request(
237236
transport = self._server_instances[request_mcp_session_id]
238237
logger.debug("Session already exists, handling request directly")
239238
# Push back idle deadline on activity
240-
if transport.idle_scope is not None:
241-
transport.idle_scope.deadline = anyio.current_time() + self._effective_idle_timeout()
239+
if transport.idle_scope is not None and self.session_idle_timeout is not None:
240+
transport.idle_scope.deadline = anyio.current_time() + self.session_idle_timeout
242241
await transport.handle_request(scope, receive, send)
243242
return
244243

@@ -271,8 +270,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
271270
# Incoming requests push the deadline forward.
272271
idle_scope = anyio.CancelScope()
273272
if self.session_idle_timeout is not None:
274-
timeout = self._effective_idle_timeout()
275-
idle_scope.deadline = anyio.current_time() + timeout
273+
idle_scope.deadline = anyio.current_time() + self.session_idle_timeout
276274
http_transport.idle_scope = idle_scope
277275

278276
with idle_scope:
@@ -332,19 +330,3 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
332330
media_type="application/json",
333331
)
334332
await response(scope, receive, send)
335-
336-
def _effective_idle_timeout(self) -> float:
337-
"""Compute the effective idle timeout, accounting for retry_interval.
338-
339-
When SSE retry_interval is configured, clients periodically reconnect
340-
to resume the event stream. A gap of up to ``retry_interval`` between
341-
connections is normal, not a sign of idleness. We use a 3x multiplier
342-
to tolerate up to two consecutive missed polls (network jitter, slow
343-
client) before considering the session idle.
344-
"""
345-
assert self.session_idle_timeout is not None
346-
timeout = self.session_idle_timeout
347-
if self.retry_interval is not None:
348-
retry_seconds = self.retry_interval / 1000.0
349-
timeout = max(timeout, retry_seconds * 3)
350-
return timeout

tests/issues/test_1283_idle_session_cleanup.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -155,25 +155,6 @@ async def test_terminate_idempotency():
155155
assert transport.is_terminated
156156

157157

158-
@pytest.mark.anyio
159-
async def test_idle_timeout_with_retry_interval():
160-
"""When retry_interval is set, effective timeout should account for polling gaps."""
161-
app = Server("test-retry-interval")
162-
163-
# retry_interval = 5000ms = 5s -> retry_seconds * 3 = 15s
164-
# session_idle_timeout = 1s -> effective = max(1, 15) = 15
165-
manager = StreamableHTTPSessionManager(app=app, session_idle_timeout=1.0, retry_interval=5000)
166-
assert manager._effective_idle_timeout() == 15.0
167-
168-
# When retry_interval is small, session_idle_timeout should dominate
169-
manager2 = StreamableHTTPSessionManager(app=app, session_idle_timeout=10.0, retry_interval=100)
170-
assert manager2._effective_idle_timeout() == 10.0
171-
172-
# No retry_interval -> raw timeout
173-
manager3 = StreamableHTTPSessionManager(app=app, session_idle_timeout=5.0)
174-
assert manager3._effective_idle_timeout() == 5.0
175-
176-
177158
@pytest.mark.anyio
178159
async def test_no_idle_timeout_sessions_persist():
179160
"""When session_idle_timeout is None (default), sessions persist indefinitely."""

0 commit comments

Comments
 (0)