Skip to content

Support session expiry controls for StreamableHTTPTransport#268

Open
koic wants to merge 1 commit intomodelcontextprotocol:mainfrom
koic:security__missing_session_expiry_controls
Open

Support session expiry controls for StreamableHTTPTransport#268
koic wants to merge 1 commit intomodelcontextprotocol:mainfrom
koic:security__missing_session_expiry_controls

Conversation

@koic
Copy link
Member

@koic koic commented Mar 23, 2026

Motivation and Context

The MCP specification recommends expiring session IDs to reduce session hijacking risks: https://modelcontextprotocol.io/specification/latest/basic/security_best_practices#session-hijacking

Several other SDKs (Go, C#, PHP, Python) already implement session expiry controls, but the Ruby SDK did not. Sessions persisted indefinitely unless explicitly deleted or a stream error occurred, leaving abandoned sessions to accumulate in memory.

This adds a session_idle_timeout: option to StreamableHTTPTransport#initialize. When set, sessions that receive no HTTP requests for the specified duration (in seconds) are automatically expired. Expired sessions return 404 on subsequent requests (GET and POST), matching the MCP specification's behavior for terminated sessions. Each request resets the idle timer, so actively used sessions are not interrupted.

A background reaper thread periodically cleans up expired sessions to handle orphaned sessions that receive no further requests. The reaper only starts when session_idle_timeout is configured.

The default is nil (no expiry) for backward compatibility, consistent with the Python SDK's approach. The Python SDK recommends 1800 seconds (30 minutes) for most deployments: modelcontextprotocol/python-sdk#2022

Resolves #265.

How Has This Been Tested?

Added tests for session expiry, idle timeout reset, reaper thread lifecycle, input validation, and default behavior in streamable_http_transport_test.rb. All existing tests continue to pass.

Breaking Change

None. The default value of session_idle_timeout is nil, which preserves the existing behavior of sessions never expiring. The new last_active_at field in the internal session hash is not part of the public API. Existing code that instantiates StreamableHTTPTransport.new(server) or
StreamableHTTPTransport.new(server, stateless: true) continues to work without changes.

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

@koic koic force-pushed the security__missing_session_expiry_controls branch 3 times, most recently from 288025a to 029e04d Compare March 23, 2026 16:53
## Motivation and Context

The MCP specification recommends expiring session IDs to reduce session hijacking risks:
https://modelcontextprotocol.io/specification/latest/basic/security_best_practices#session-hijacking

Several other SDKs (Go, C#, PHP, Python) already implement session expiry controls, but
the Ruby SDK did not. Sessions persisted indefinitely unless explicitly deleted or
a stream error occurred, leaving abandoned sessions to accumulate in memory.

This adds a `session_idle_timeout:` option to `StreamableHTTPTransport#initialize`. When
set, sessions that receive no HTTP requests for the specified duration (in seconds) are
automatically expired. Expired sessions return 404 on subsequent requests (GET and POST),
matching the MCP specification's behavior for terminated sessions. Each request resets
the idle timer, so actively used sessions are not interrupted.

A background reaper thread periodically cleans up expired sessions to handle orphaned
sessions that receive no further requests. The reaper only starts when
`session_idle_timeout` is configured.

The default is `nil` (no expiry) for backward compatibility, consistent with the Python
SDK's approach. The Python SDK recommends 1800 seconds (30 minutes) for most deployments:
modelcontextprotocol/python-sdk#2022

Resolves modelcontextprotocol#265.

## How Has This Been Tested?

Added tests for session expiry, idle timeout reset, reaper thread lifecycle, input
validation, and default behavior in `streamable_http_transport_test.rb`. All existing
tests continue to pass.

## Breaking Change

None. The default value of `session_idle_timeout` is `nil`, which preserves the existing
behavior of sessions never expiring. The new `last_active_at` field in the internal
session hash is not part of the public API. Existing code that instantiates
`StreamableHTTPTransport.new(server)` or
`StreamableHTTPTransport.new(server, stateless: true)` continues to work without changes.
@koic koic force-pushed the security__missing_session_expiry_controls branch from 029e04d to 25830a2 Compare March 23, 2026 16:54
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.

Add session timeouts

1 participant