Commit 5ed3b5d
committed
Support POST response SSE streams for server-to-client messages
## 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, the Ruby SDK only supported server-to-client messages via a separate GET SSE stream,
and POST responses to JSON-RPC requests were sent through the GET stream with a 202 HTTP status.
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. This made the SDK non-compliant with the specification.
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[: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, falling back to
the GET SSE stream for messages without a related request.
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
Updated 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-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.1 parent 0d700d7 commit 5ed3b5d
File tree
10 files changed
+617
-154
lines changed- lib
- mcp
- server/transports
- test
- mcp
- server/transports
10 files changed
+617
-154
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
95 | | - | |
| 95 | + | |
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
127 | 127 | | |
128 | 128 | | |
129 | 129 | | |
130 | | - | |
131 | | - | |
| 130 | + | |
| 131 | + | |
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
| |||
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
143 | | - | |
144 | | - | |
| 143 | + | |
| 144 | + | |
145 | 145 | | |
146 | 146 | | |
147 | 147 | | |
| |||
220 | 220 | | |
221 | 221 | | |
222 | 222 | | |
223 | | - | |
| 223 | + | |
| 224 | + | |
224 | 225 | | |
225 | 226 | | |
226 | 227 | | |
| |||
371 | 372 | | |
372 | 373 | | |
373 | 374 | | |
374 | | - | |
| 375 | + | |
375 | 376 | | |
376 | 377 | | |
377 | 378 | | |
| |||
399 | 400 | | |
400 | 401 | | |
401 | 402 | | |
402 | | - | |
| 403 | + | |
403 | 404 | | |
404 | 405 | | |
405 | 406 | | |
| |||
499 | 500 | | |
500 | 501 | | |
501 | 502 | | |
502 | | - | |
| 503 | + | |
503 | 504 | | |
504 | 505 | | |
505 | 506 | | |
| |||
531 | 532 | | |
532 | 533 | | |
533 | 534 | | |
534 | | - | |
| 535 | + | |
535 | 536 | | |
536 | 537 | | |
537 | 538 | | |
| |||
611 | 612 | | |
612 | 613 | | |
613 | 614 | | |
614 | | - | |
| 615 | + | |
615 | 616 | | |
616 | 617 | | |
617 | 618 | | |
618 | | - | |
619 | | - | |
| 619 | + | |
| 620 | + | |
620 | 621 | | |
621 | 622 | | |
622 | 623 | | |
| |||
0 commit comments