Add SSE polling support (SEP-1699)
Summary
Implements SEP-1699 which enables servers to disconnect SSE connections at will by sending priming events and retry fields.
Motivation and Context
SEP-1699 introduces SSE polling behavior that allows servers to control client reconnection timing and close connections gracefully. This enables more efficient resource management on the server side while maintaining resumability.
We implement this on the POST SSE stream as implied by the SEP language linked above. I.e. when a server establishes an SSE stream:
- It's first message will be an event including no data, only an event ID.
- After that, it may call
close_sse_streamto close the stream while still gathering the events. - The client can start "polling" the SSE stream based on the
retryIntervalsupplied by the server before disconnection.
How Has This Been Tested?
- Added e2e integration tests demonstrating the initial priming message, disconnection initiated by the server, and subsequent reconnection via polling by the client.
- Example server & client demonstrating polling
- [currently WIP] upcoming conformance tests in https://github.com/modelcontextprotocol/conformance/pull/47
Example server and client:
Breaking Changes
None. Client falls back to exponential backoff if no retry field is provided.
Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
Checklist
- [x] I have read the MCP Documentation
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed
Reworking this from scratch.
Reworked this from the ground by going from tests first.
The best way to review this is probably look at the tests in tests/shared/test_streamable_http.py which describe the expected behavior introduced by SEP-1699
Then looking through server/streamable_http.py where we introduced close_sse_stream and streamable_http.py where we actually perform the automatic reconnection via GET stream in _handle_reconnection
The close_sse_stream is available via the RequestContext so the server doesn't have to reach down into the transport.
I updated the examples as well to clearly demonstrate how a stream can be disconnected by the server, triggering a reconnection & finally completing the request: