Support POST response SSE streams for server-to-client messages#294
Merged
koic merged 1 commit intomodelcontextprotocol:mainfrom Apr 4, 2026
Merged
Conversation
46aa0fa to
affbda4
Compare
## Motivation and Context The MCP Streamable HTTP specification defines that servers can return POST responses as SSE streams and send server-to-client JSON-RPC requests and notifications through them: > If the input is a JSON-RPC request, the server MUST either return > `Content-Type: text/event-stream`, to initiate an SSE stream, or > `Content-Type: application/json`, to return one JSON object. If the server initiates an SSE stream: > The server MAY send JSON-RPC requests and notifications before > sending the JSON-RPC response. These messages SHOULD relate to the > originating client request. See: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server Previously, when a GET SSE stream was connected, the Ruby SDK sent POST response bodies through the GET stream and returned a 202 HTTP status for the POST itself. When no GET SSE stream was connected, the SDK correctly returned `application/json` responses. The 202 behavior with GET SSE was non-compliant with the specification, which requires POST requests to return `text/event-stream` or `application/json`. Additionally, since GET SSE is optional per the specification, clients that did not establish a GET SSE connection could not receive server-to-client messages (e.g., `sampling/createMessage`, log notifications) during request processing. With this change, `handle_regular_request` always returns the POST response as an SSE stream for stateful sessions. Each POST response stream is stored in `session[:post_request_streams]` keyed by `related_request_id` (the JSON-RPC request ID), enabling correct routing when multiple POST requests are processed concurrently on the same session. Server-to-client messages with a `related_request_id` are routed to the originating POST response stream only; when `related_request_id` is nil, the GET SSE stream is used. The TypeScript and Python SDKs already support this pattern. ### Internal Changes `JsonRpcHandler.handle` and `JsonRpcHandler.handle_json` now pass both `method_name` and `request_id` to the method finder block. This allows `Server#handle_request` to receive `related_request_id` directly from the protocol layer. Without this, `related_request_id` would need to be relayed as a keyword argument through `Server#handle_json`, `ServerSession#handle_json`, and `dispatch_handle_json`, unnecessarily exposing it on public method signatures. This follows the same design as the TypeScript and Python SDKs, where the protocol layer extracts the request ID and propagates it to the handler context. ## How Has This Been Tested? Added tests for POST response stream: - `send_request` via POST response stream (sampling with and without GET SSE) - `send_notification` via POST response stream (logging without GET SSE) - `progress` notification via POST response stream (without GET SSE) - POST request returns SSE response even with GET SSE connected - Session-scoped notifications (log, progress) are sent to POST response stream, not GET SSE stream - `active_stream` does not fall back to GET SSE when `related_request_id` is given but request stream is missing Updated existing tests to handle SSE response format where applicable. ## Breaking Changes (spec compliance fix) POST responses for JSON-RPC requests in stateful sessions now return `Content-Type: text/event-stream` instead of being sent through the GET SSE stream with a 202 HTTP status. Clients that relied on receiving responses via the GET SSE stream will need to read the POST response body instead. This is a spec compliance fix: the MCP specification requires POST requests to return `text/event-stream` or `application/json`, not 202 with the response on a separate GET stream.
affbda4 to
8278ca6
Compare
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
The MCP Streamable HTTP specification defines that servers can return POST responses as SSE streams and send server-to-client JSON-RPC requests and notifications through them:
If the server initiates an SSE stream:
See: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server
Previously, when a GET SSE stream was connected, the Ruby SDK sent POST response bodies through the GET stream and returned a 202 HTTP status for the POST itself. When no GET SSE stream was connected, the SDK correctly returned
application/jsonresponses. The 202 behavior with GET SSE was non-compliant with the specification, which requires POST requests to returntext/event-streamorapplication/json. Additionally, since GET SSE is optional per the specification, clients that did not establish a GET SSE connection could not receive server-to-client messages (e.g.,sampling/createMessage, log notifications) during request processing.With this change,
handle_regular_requestalways returns the POST response as an SSE stream for stateful sessions. Each POST response stream is stored insession[:post_request_streams]keyed byrelated_request_id(the JSON-RPC request ID), enabling correct routing when multiple POST requests are processed concurrently on the same session. Server-to-client messages with arelated_request_idare routed to the originating POST response stream only; whenrelated_request_idis nil, the GET SSE stream is used.The TypeScript and Python SDKs already support this pattern.
Internal Changes
JsonRpcHandler.handleandJsonRpcHandler.handle_jsonnow pass bothmethod_nameandrequest_idto the method finder block. This allowsServer#handle_requestto receiverelated_request_iddirectly from the protocol layer. Without this,related_request_idwould need to be relayed as a keyword argument throughServer#handle_json,ServerSession#handle_json, anddispatch_handle_json, unnecessarily exposing it on public method signatures. This follows the same design as the TypeScript and Python SDKs, where the protocol layer extracts the request ID and propagates it to the handler context.How Has This Been Tested?
Added tests for POST response stream:
send_requestvia POST response stream (sampling with and without GET SSE)send_notificationvia POST response stream (logging without GET SSE)progressnotification via POST response stream (without GET SSE)active_streamdoes not fall back to GET SSE whenrelated_request_idis given but request stream is missingUpdated existing tests to handle SSE response format where applicable.
Breaking Changes
This PR is a spec compliance fix.
POST responses for JSON-RPC requests in stateful sessions now return
Content-Type: text/event-streaminstead of being sent through the GET SSE stream with a 202 HTTP status. Clients that relied on receiving responses via the GET SSE stream will need to read the POST response body instead. This is a spec compliance fix: the MCP specification requires POST requests to returntext/event-streamorapplication/json, not 202 with the response on a separate GET stream.Types of changes
Checklist