Skip to content

fix: expose session_idle_timeout in streamable_http_app and guard active requests#2496

Open
Christian-Sidak wants to merge 2 commits intomodelcontextprotocol:mainfrom
Christian-Sidak:fix/issue-2455
Open

fix: expose session_idle_timeout in streamable_http_app and guard active requests#2496
Christian-Sidak wants to merge 2 commits intomodelcontextprotocol:mainfrom
Christian-Sidak:fix/issue-2455

Conversation

@Christian-Sidak
Copy link
Copy Markdown

Summary

Fixes two linked bugs with session_idle_timeout in StreamableHTTPSessionManager:

Problem 1 — not exposed by streamable_http_app()

StreamableHTTPSessionManager accepts session_idle_timeout, but neither
Server.streamable_http_app() (lowlevel) nor MCPServer.streamable_http_app()
accepted or forwarded it, causing an immediate TypeError for anyone using the
high-level API.

Problem 2 — active requests cancelled by the idle timeout

The idle anyio.CancelScope wrapped the entire app.run() call. Incoming
requests pushed the deadline forward, but there was no guard that suspended the
scope while a request was in-flight. A handler that took longer than the
timeout from the last deadline-push would be cancelled mid-execution.

Changes

  • Add session_idle_timeout: float | None = None to Server.streamable_http_app(),
    MCPServer.streamable_http_app(), and MCPServer.run_streamable_http_async()
    and thread it through to StreamableHTTPSessionManager.
  • In _handle_stateful_request, set idle_scope.deadline = math.inf before
    calling transport.handle_request() (both existing-session and new-session
    paths) and reset to now + session_idle_timeout in a finally block,
    ensuring the idle clock only ticks between requests.

Tests added

  • test_streamable_http_app_accepts_session_idle_timeout — verifies the parameter
    is forwarded to the session manager.
  • test_active_request_not_cancelled_by_idle_timeout — 50 ms idle timeout,
    150 ms handler: asserts the handler completes successfully.
  • test_idle_session_reaped_after_request_completes — asserts idle reaping still
    works once requests are finished.

Fixes #2455

…ctive request cancellation

- Add session_idle_timeout parameter to Server.streamable_http_app(),
  MCPServer.streamable_http_app(), and run_streamable_http_async() so
  users of the high-level API can configure idle session reaping without
  dropping down to manual StreamableHTTPSessionManager wiring.
- Suspend the idle CancelScope deadline (set to math.inf) while a
  request is in-flight and reset it after, preventing a slow handler
  from being cancelled mid-execution by the idle timeout.

Fixes modelcontextprotocol#2455

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Christian-Sidak
Copy link
Copy Markdown
Author

Friendly bump -- let me know if anything needs changing.

…session path

The finally block mirrors the condition checked immediately before the try:
block. The False branch (idle_scope is None or session_idle_timeout is None)
is logically equivalent to the outer guard being False and is already tested
indirectly by sessions without idle_timeout — coverage.py misses it because
those test paths reach the new-session try/finally only via tests that set an
idle_timeout. Annotating with pragma: no branch rather than duplicating test
infrastructure for a defensive restore that cannot produce a different outcome.
@Christian-Sidak
Copy link
Copy Markdown
Author

Pushed a one-line fix for the coverage failure (99.99% → 100%): the finally block's guard mirrors the check immediately before the try:, so the False branch is structurally unreachable in tests but not logically incorrect. Annotated with # pragma: no branch to suppress the report. All 1175 tests continue to pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

session_idle_timeout is not exposed via streamable_http_app() and can cancel active requests

2 participants