Title: Enable session roaming across multiple server instances by davidroberts-merlyn · Pull Request #1519 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: Enable session roaming across multiple server instances by davidroberts-merlyn · Pull Request #1519 · modelcontextprotocol/python-sdk
X Title: Enable session roaming across multiple server instances by davidroberts-merlyn · Pull Request #1519 · modelcontextprotocol/python-sdk
Description: Motivation and Context Problem When deploying MCP servers across multiple instances (Kubernetes pods, Docker containers, worker processes), sessions are tied to the specific instance that created them. This requires sticky sessions at the load balancer level and prevents true horizontal scaling. Users are currently forced to choose between: Sticky sessions - Suboptimal load distribution, sessions lost on pod failure Single worker - Wastes resources, limits throughput Stateless mode - Loses session continuity and event replay This limitation is documented in multiple issues: #520 (multi-worker sessions), #692 (session reuse across instances), #880 (horizontal scalability), and #1350 (sticky session problems). Solution This PR enables session roaming - allowing sessions to seamlessly move between server instances without requiring sticky sessions. The key insight is that EventStore already serves as proof of session existence. When a request arrives with a session ID that's not in an instance's local memory, if an EventStore is configured, the instance can safely: Create a transport for that session ID (session roaming) Let EventStore replay any missed events (continuity) Handle the request seamlessly What Changed Modified streamable_http_manager.py (~50 lines): Added session roaming logic in _handle_stateful_request() When unknown session ID + EventStore exists → create transport (roaming!) Extracted duplicate server task code into reusable methods Updated docstrings to document session roaming capability Added comprehensive tests (test_session_roaming.py, 510 lines): Session roaming with EventStore Rejection without EventStore Concurrent request handling Exception cleanup behavior Fast path verification Logging verification Added production-ready example (simple-streamablehttp-roaming/, 13 files): Complete working example with Redis EventStore Multi-instance deployment support Docker Compose configuration (3 instances + Redis + NGINX) Kubernetes deployment example Automated test script demonstrating roaming Comprehensive documentation (README, QUICKSTART, implementation details) Why This Approach Previous Attempts Eplored two other approaches before arriving at this solution: Custom Session Store (outside SDK) - Created own session validation in the application layer, but this didn't solve the core problem and required every user to implement their own solution, it also meant that as the dict that contained session in the sdk was unchanged it still required sticky sessions. SessionStore ABC (in SDK) - Added a new SessionStore interface requiring both EventStore + SessionStore parameters. While functional, this approach required two separate storage backends and was more complex than necessary. It also meant that if you did not supply one of the stores it was not really stateful. Current Approach: EventStore-Only The key insight: EventStore already proves sessions existed. If events exist for a session ID, that session must have existed to create those events. No separate SessionStore needed. Benefits: ✅ One store instead of two (simpler) ✅ Reuses existing EventStore interface (no new APIs) ✅ Impossible to misconfigure (EventStore = both events + proof) ✅ Aligns with SEP-1359 (sessions are conversation context, not auth) ✅ Minimal code changes (~50 lines) ✅ 100% backward compatible (behavior enhancement only) Usage Before (Requires Sticky Sessions) # Without EventStore - sessions in memory only manager = StreamableHTTPSessionManager(app=app) # Deployment: requires sticky sessions for multi-instance After (No Sticky Sessions Needed) # With EventStore - sessions roam freely event_store = RedisEventStore(redis_url="redis://redis:6379") manager = StreamableHTTPSessionManager( app=app, event_store=event_store # Enables session roaming! ) # Deployment: load balancer can route freely, no sticky sessions How It Works Client → Instance 1 (creates session "abc123", stores events in Redis) Client → Instance 2 (with session "abc123") ↓ Instance 2 checks memory → not found Instance 2 sees EventStore exists Instance 2 creates transport for "abc123" (roaming!) EventStore replays events from Redis Session continues seamlessly ✅ How Has This Been Tested? ✅ Existing test suite (no regressions) ✅ 8 new tests for session roaming ✅ Automated roaming test script in example ✅ Testing within our existing infrastructure The included example also demonstrates: Multi-instance deployment with Docker Compose Kubernetes manifests (3 replicas, no sessionAffinity needed) NGINX load balancing without sticky sessions Redis EventStore for shared state Automated testing and verification with provided test script Breaking Changes None. This is a pure behavior enhancement: ✅ Existing code works unchanged ✅ No API changes ✅ No new required parameters ✅ Backward compatible 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 Additional context Related Issues Closes #520, #692, #880, #1350 This implementation addresses the core limitation described in all these issues: the inability to run stateful MCP servers across multiple instances without sticky sessions.
Open Graph Description: Motivation and Context Problem When deploying MCP servers across multiple instances (Kubernetes pods, Docker containers, worker processes), sessions are tied to the specific instance that created t...
X Description: Motivation and Context Problem When deploying MCP servers across multiple instances (Kubernetes pods, Docker containers, worker processes), sessions are tied to the specific instance that created t...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/1519
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:0e7b37ae-0c49-6837-4995-7287bb329c4e |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | A1EA:1E71B6:D8C8A1:1339626:6A5A1F86 |
| html-safe-nonce | 14f57d6c0ebfc9ce6655c2277ea8f3706d2ff75d5dd9802f226a48e517d53d27 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJBMUVBOjFFNzFCNjpEOEM4QTE6MTMzOTYyNjo2QTVBMUY4NiIsInZpc2l0b3JfaWQiOiI1NDczMTM2MDUwMzgxMTM1NzUwIiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | 13336e986eeb33a92a812190a685a2af210ecd8af3927f6d02ae26808bde0a65 |
| hovercard-subject-tag | pull_request:2950652435 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelcontextprotocol/python-sdk/pull/1519/files |
| twitter:image | https://avatars.githubusercontent.com/u/121063480?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/121063480?s=400&v=4 |
| og:image:alt | Motivation and Context Problem When deploying MCP servers across multiple instances (Kubernetes pods, Docker containers, worker processes), sessions are tied to the specific instance that created t... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | e31742e80bbd077fd5679c13ed870ce4e0c13803a2fa5beaae557b6769de2c8a |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | c9fcdbf98c619d1561ed843fa01d9766a32b5cd9 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width