Skip to content

fix(streamable-http): expose session_idle_timeout and pause idle reaping during active requests#2457

Open
shaun0927 wants to merge 4 commits intomodelcontextprotocol:mainfrom
shaun0927:fix/streamable-http-idle-timeout
Open

fix(streamable-http): expose session_idle_timeout and pause idle reaping during active requests#2457
shaun0927 wants to merge 4 commits intomodelcontextprotocol:mainfrom
shaun0927:fix/streamable-http-idle-timeout

Conversation

@shaun0927
Copy link
Copy Markdown

Fixes #2455

Summary

This PR fixes two linked session_idle_timeout problems in the StreamableHTTP stack:

  1. expose session_idle_timeout through the public streamable_http_app() entrypoints
  2. suspend idle reaping while one or more requests are actively in flight

Motivation and Context

PR #1994 / #2022 describe session_idle_timeout as a way to reap sessions that receive no HTTP requests for the configured duration.

Current main has two mismatches with that contract:

  • StreamableHTTPSessionManager(...) accepts session_idle_timeout, but Server.streamable_http_app(...) and MCPServer.streamable_http_app(...) do not, so the canonical high-level API cannot configure the feature
  • the idle deadline can fire while a request is still being processed, terminating the session mid-handler even though the session is not idle in any practical sense

Changes

  • add session_idle_timeout passthrough to:
    • mcp.server.lowlevel.server.Server.streamable_http_app()
    • mcp.server.mcpserver.server.MCPServer.streamable_http_app()
  • track active request count on StreamableHTTPServerTransport
  • suspend idle reaping while requests are active
  • restore the idle deadline only after the last active request finishes
  • initialize the idle scope before the first request can complete

How Has This Been Tested?

Added focused regression coverage in tests/server/test_streamable_http_manager.py:

  • test_streamable_http_app_exposes_session_idle_timeout
  • test_session_idle_timeout_does_not_cancel_in_flight_request

The second test uses a handler that runs longer than the configured idle timeout and verifies that the request still completes successfully.

Existing idle reaping coverage (test_idle_session_is_reaped) continues to pass, so finished sessions are still cleaned up normally.

Local verification:

  • uv run --frozen pytest tests/server/test_streamable_http_manager.py
  • uv run --frozen ruff check src/mcp/server/lowlevel/server.py src/mcp/server/mcpserver/server.py src/mcp/server/streamable_http.py src/mcp/server/streamable_http_manager.py tests/server/test_streamable_http_manager.py
  • uv run --frozen pyright src/mcp/server/lowlevel/server.py src/mcp/server/mcpserver/server.py src/mcp/server/streamable_http.py src/mcp/server/streamable_http_manager.py

Breaking Changes

None.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

The StreamableHTTP idle-timeout feature was implemented on the session manager but not exposed through the canonical streamable_http_app() entrypoints, pushing users toward lower-level wiring. More importantly, the idle deadline could fire while a request was still in flight, terminating the session mid-handler even though the feature is documented as reaping sessions that receive no HTTP requests. This threads session_idle_timeout through the public app builders, pauses idle reaping while requests are active, restores the deadline after the last request completes, and adds regression coverage for both passthrough and long-running request behavior.

Constraint: Must preserve existing idle-session cleanup semantics once no requests remain in flight
Rejected: Expose session_idle_timeout without changing runtime semantics | leaves the documented feature behavior observably incorrect
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: If idle reaping changes again, verify both public API reachability and active-request semantics together
Tested: uv run --frozen pytest tests/server/test_streamable_http_manager.py; uv run --frozen ruff check src/mcp/server/lowlevel/server.py src/mcp/server/mcpserver/server.py src/mcp/server/streamable_http.py src/mcp/server/streamable_http_manager.py tests/server/test_streamable_http_manager.py; uv run --frozen pyright src/mcp/server/lowlevel/server.py src/mcp/server/mcpserver/server.py src/mcp/server/streamable_http.py src/mcp/server/streamable_http_manager.py
Not-tested: Full integration matrix outside the focused StreamableHTTP manager suite
The new session_idle_timeout tests started exercising two paths the existing source annotations and timing assumptions no longer matched. CI's strict-no-cover gate now observes the session_manager return path, and the idle-reap assertion can race under slower coverage-instrumented runs. This follow-up removes the stale pragma and makes the idle-reap assertion wait for actual session removal before checking the 404 behavior.

Constraint: Must preserve the PR's runtime semantics and only address CI-validity drift
Rejected: Increase fixed sleeps again | keeps the test timing-sensitive and flaky under slower environments
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Prefer state-based waits over fixed sleeps in StreamableHTTP timeout tests
Tested: uv run --frozen pytest tests/server/test_streamable_http_manager.py; coverage run of the same suite; strict-no-cover-sensitive paths exercised locally
Not-tested: Full upstream matrix rerun after push
The StreamableHTTP follow-up test was logically correct but not yet aligned with the repository's stricter pyright settings in CI. This updates the test to use the typed Tool field name, annotate the ASGI handler parameters explicitly, and narrow the tool result content before reading .text. The runtime behavior under test is unchanged.

Constraint: Must keep the fix scoped to type-check compliance for the new regression test
Rejected: Silence pyright with ignores | conflicts with repo guidance and hides actionable typing issues
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: New regression tests in this repo should satisfy the same pyright strictness as production code
Tested: uv run --frozen pytest tests/server/test_streamable_http_manager.py; uv run --frozen ruff check tests/server/test_streamable_http_manager.py; uv run --frozen pyright tests/server/test_streamable_http_manager.py
Not-tested: Full upstream CI rerun after push
@shaun0927
Copy link
Copy Markdown
Author

for additional signal: mcp-claude[bot] independently reproduced both bugs and proposed the same suspend-then-reset pattern around handle_request plus forwarding session_idle_timeout from streamable_http_app() (issue #2455 comment). the one failing job (test_stdio_client_bad_path on 3.10/ubuntu) is unrelated to this PR's surface (this PR doesn't touch stdio_client) — it's a flaky ConnectionResetError in stdin_writer. retriggering now.

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

1 participant