Skip to content

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

Description

@shaun0927

Summary

The session_idle_timeout feature currently has two linked problems:

  1. it is implemented on StreamableHTTPSessionManager but not exposed through the canonical streamable_http_app() API surface
  2. it can terminate a session while a request is actively in flight

Why this looks like a bug

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

That implies two things:

  • users of the high-level StreamableHTTP API should be able to configure it without dropping down to manual session-manager wiring
  • a request that is currently being processed should not count as an idle session

Today neither of those expectations holds.

Problem 1: not exposed by streamable_http_app()

StreamableHTTPSessionManager(...) accepts session_idle_timeout, but:

  • mcp.server.lowlevel.server.Server.streamable_http_app(...) does not
  • mcp.server.mcpserver.server.MCPServer.streamable_http_app(...) does not

So the recommended high-level API cannot configure a supported session-manager feature.

Reproduction

from mcp.server import Server

app = Server("demo")
app.streamable_http_app(session_idle_timeout=30)

Observed behavior

TypeError: Server.streamable_http_app() got an unexpected keyword argument 'session_idle_timeout'

Problem 2: active requests can be cancelled by the idle timeout

With a short idle timeout and a handler that takes longer than that timeout, the session can be reaped mid-request and the client waits until timeout / session termination rather than receiving the tool result.

Reproduction outline

  • configure StreamableHTTPSessionManager(app=..., session_idle_timeout=...)
  • make a tool handler sleep longer than the timeout
  • call the tool over StreamableHTTP

Observed behavior

On current main, the active request can be terminated before the handler finishes, and the client fails instead of receiving the result.

Expected behavior

  • streamable_http_app() should expose session_idle_timeout
  • idle reaping should only happen when the session has no in-flight requests
  • once the last active request finishes, the idle deadline can resume normally

Validation

I reproduced both behaviors locally against current main.

I also verified that a minimal patch can fix both together by:

  1. threading session_idle_timeout through the low-level + MCPServer streamable_http_app() wrappers
  2. suspending idle reaping while at least one request is in flight
  3. restoring the idle deadline after the last active request completes

A focused regression suite in tests/server/test_streamable_http_manager.py can cover:

  • passthrough from streamable_http_app(session_idle_timeout=...)
  • active request longer than the timeout still completes successfully
  • existing idle-session reaping still works once requests are finished

Activity

  1. mcp-claude commented on Apr 17, 2026

    @mcp-claude

    two bugs in the session_idle_timeout implementation, both reproduced on current main and v1.x.

    problem 1: streamable_http_app() silently drops session_idle_timeout — it's accepted by StreamableHTTPSessionManager.__init__ but not forwarded by either Server.streamable_http_app() (lowlevel) or MCPServer.streamable_http_app(), causing an immediate TypeError.

    problem 2: the idle anyio.CancelScope wraps the entire app.run() call. incoming requests push the deadline forward, but there's no guard that suspends the scope while a request is in-flight. if a handler takes longer than session_idle_timeout from the last push, the scope fires and the handler is cancelled mid-execution.

    workaround for problem 1: construct StreamableHTTPSessionManager directly instead of using streamable_http_app().
    workaround for problem 2: none short of setting a very generous timeout.

    repro scripts + output

    repro.py (problem 1):

    from mcp.server import Server
    
    app = Server("demo")
    app.streamable_http_app(session_idle_timeout=30)

    output:

    TypeError: Server.streamable_http_app() got an unexpected keyword argument 'session_idle_timeout'
    

    repro_p2.py (problem 2 — replicates the CancelScope pattern from streamable_http_manager.py:232-243):

    import anyio
    
    IDLE_TIMEOUT = 0.3  # 300ms — shorter than the simulated handler's 2s sleep
    
    async def main():
        idle_scope = anyio.CancelScope()
        # simulate: request arrives, deadline pushed forward (manager.py:200-201)
        idle_scope.deadline = anyio.current_time() + IDLE_TIMEOUT
    
        with idle_scope:
            print("  [scope] handler started")
            await anyio.sleep(2.0)  # simulates slow tool handler
            print("  [scope] handler completed")  # never reached
    
        if idle_scope.cancelled_caught:
            print("REPRODUCED: handler cancelled mid-execution")
    
    anyio.run(main)
      [scope] handler started
    REPRODUCED: CancelScope fired while handler was sleeping — handler never completed
    

    code path (problem 2):

    • streamable_http_manager.py:232-235: creates CancelScope with deadline = now + session_idle_timeout
    • streamable_http_manager.py:200-201: on each request, pushes deadline to now + session_idle_timeout
    • streamable_http_manager.py:237-243: with idle_scope: await self.app.run(...) — the scope is never suspended during request handling
    • when deadline expires the scope cancels app.run() regardless of active handlers
    suggested fix
    // src/mcp/server/lowlevel/server.py
     def streamable_http_app(
         self,
         *,
         streamable_http_path: str = "/mcp",
         json_response: bool = False,
         stateless_http: bool = False,
         event_store: EventStore | None = None,
         retry_interval: int | None = None,
    +    session_idle_timeout: float | None = None,
         transport_security: TransportSecuritySettings | None = None,
         ...
     ):
         session_manager = StreamableHTTPSessionManager(
             ...
    +        session_idle_timeout=session_idle_timeout,
         )
    
    // src/mcp/server/mcpserver/server.py  (identical signature + forward change)
    
    // src/mcp/server/streamable_http_manager.py
    +import math
     ...
    -# Push back idle deadline on activity
    -if transport.idle_scope is not None and self.session_idle_timeout is not None:
    -    transport.idle_scope.deadline = anyio.current_time() + self.session_idle_timeout  # pragma: no cover
    -await transport.handle_request(scope, receive, send)
    +# Suspend idle deadline while the request is in-flight so a slow
    +# handler cannot be cancelled mid-execution, then reset after.
    +if transport.idle_scope is not None and self.session_idle_timeout is not None:
    +    transport.idle_scope.deadline = math.inf
    +try:
    +    await transport.handle_request(scope, receive, send)
    +finally:
    +    if transport.idle_scope is not None and self.session_idle_timeout is not None:
    +        transport.idle_scope.deadline = anyio.current_time() + self.session_idle_timeout

    same suspend/reset pattern applied to the new-session path (http_transport.handle_request call after task_group.start).

    test to verify: creates a server with session_idle_timeout=0.05s and a list_tools handler that sleeps 0.15s, then asserts the handler completes and the client call succeeds.

  2. added
    bugSomething isn't working
    ready for workEnough information for someone to start working on
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    fix proposedBot has a verified fix diff in the comment
    on Apr 17, 2026
  3. shaun0927 commented on Apr 17, 2026

    @shaun0927
    Author

    thanks for the independent confirmation of both bugs. the suggested fix matches PR #2457 (suspend-then-reset the idle deadline around handle_request + forward session_idle_timeout from both Server.streamable_http_app() and MCPServer.streamable_http_app()). PR is up with a regression test that uses a 50ms idle timeout against a 150ms handler — feedback welcome.

  4. buildwithabid commented on Apr 27, 2026

    @buildwithabid

    Picking this up. Approach matches the validation outline in the issue:

    1. Plumb session_idle_timeout through both streamable_http_app() wrappers (low-level Server and MCPServer).
    2. Suspend the idle deadline while at least one request is in flight. Using a per-transport idle_active_requests counter; on first increment set idle_scope.deadline = math.inf, on drop to 0 reset to now + session_idle_timeout. This lets long-running tool calls finish even when they exceed the timeout.
    3. Tests in tests/server/test_streamable_http_manager.py covering: (a) passthrough from streamable_http_app(session_idle_timeout=...) on both wrappers, (b) handler running longer than the timeout still completes, (c) existing idle reaping behavior intact once requests are finished, (d) deadline only resumes after the last concurrent request completes.

    PR incoming.

  5. finedesignz commented on Aug 19, 2026

    @finedesignz

    Production data point in support of this issue, from a FastMCP streamable-HTTP server
    (mcp 1.27.1, uvicorn, loopback only, many short-lived MCP clients).

    Over a single 18.2 hour process lifetime the server logged:

    Created new transport with session ID: ...   224
    Session ... idle timeout                       0
    Cleaning up crashed session ...                0
    DELETE /mcp                                    0
    POST /mcp HTTP/1.1" 200                      461
    POST /mcp HTTP/1.1" 202                      115
    POST /mcp HTTP/1.1" 400                      109
    

    224 sessions created, zero ever removed. Only 57 client sockets were still established at
    the end, so roughly 167 StreamableHTTPServerTransport objects and their run_server
    tasks were retained with no client behind them, for the lifetime of the process. Growth
    was steady at about 12 leaked sessions per hour and is bounded only by process restarts.

    Cause, on 1.27.1:

    • mcp/server/streamable_http_manager.py:268-269 only arms the idle cancel scope when
      self.session_idle_timeout is not None.
    • mcp/server/fastmcp/server.py:956-962 builds the manager as
      StreamableHTTPSessionManager(app=..., event_store=..., retry_interval=..., json_response=..., stateless=..., security_settings=...) with no session_idle_timeout,
      and FastMCP.settings has no field for it, so for every FastMCP streamable-HTTP server
      the value is unconditionally None.
    • The class docstring at streamable_http_manager.py:58-62 recommends 1800 for most
      deployments, which no FastMCP user can currently set.

    A client that goes away without sending DELETE /mcp (process killed, laptop closed,
    container reaped) therefore leaks its transport, its task and its SSE stream permanently.

    Suggested minimal fix for the FastMCP side: add session_idle_timeout: float | None = None
    to Settings and pass session_idle_timeout=self.settings.session_idle_timeout in the
    StreamableHTTPSessionManager(...) call at fastmcp/server.py:956. The same argument
    applies to mcp/server/lowlevel/server.py:746 in 2.0.0, where streamable_http_app()
    takes json_response, stateless_http, event_store, retry_interval,
    max_request_body_size, transport_security and host but still not
    session_idle_timeout, so 2.0.0 inherits the same defect.

    Workaround currently in use, for anyone hitting this on 1.x, using only public surface:

    mcp.streamable_http_app()                                  # create the manager eagerly
    mcp.session_manager.session_idle_timeout = 1800.0          # public attribute
    mcp.run(transport="streamable-http")                       # reuses the same manager

    Verified: with the timeout set to 3 seconds, an initialized session logs
    Session <id> idle timeout three seconds later and is removed from _server_instances.
    Without it, nothing is ever logged.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workingfix proposedBot has a verified fix diff in the commentready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions