Repository navigation
fix: return from initialize as soon as its SSE response arrives - #617
Merged
koic merged 1 commit intoOct 17, 2026
Merged
Conversation
atesgoral
force-pushed
the
fix/initialize-sse-abort
branch
from
October 15, 2026 13:49
348e6b3 to
80ecebd
Compare
atesgoral
marked this pull request as draft
October 15, 2026 13:55
atesgoral
force-pushed
the
fix/initialize-sse-abort
branch
from
October 15, 2026 14:05
80ecebd to
43dc0f8
Compare
atesgoral
marked this pull request as ready for review
October 15, 2026 15:42
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.
Problem
MCP::Client::HTTP#send_requeststops an SSE stream when the JSON-RPC response event arrives. It does not stop the stream forinitialize:The exception lets
capture_session_inforead the finishedFaraday::Response. The response holds theMcp-Session-Idheader. If the client stops the stream early,postdoes not return a response object. Session capture then has no response to read.Some servers send the
initializeresult over SSE and then keep the stream open. GitMCP is one example. GitMCP is a public stateful server. It sends responses astext/event-stream. It rejects later requests that do not carry the initialize session id. The response arrives in under 0.7 s. The stream stays open for about 10 s (curl: ttfb 0.67 s, total 10.7 s). A client pays about 10 s for each new session. Initialize plus a first call takes about 21 s. The useful work takes under one second. All other methods already return when the response event arrives.Change
Read the response headers at the point where the code already reads them. Do not wait for the finished response object.
SSEStream#on_datasavesenv.response_headerswhen it first sees them. The sameenvgives the content type check fortext/event-stream.abortable: trueforinitializealso.capture_session_infotakes headers, not the response object.send_requestpasses the response headers when a response exists. It passes the headers saved by the stream when the early stop removed the response.raise_errormiddleware in Faraday. The OAuth and step-up retries insend_requestthen run as before. Error responses are read to the end of the stream. This guard has a regression test below. Without the guard, a 401 with a JSON-RPC error event in the SSE body could return as a normal response. The retry would not run.Fallbacks
Behavior does not change when stream headers are not available:
on_datawithoutenv. Seetest_sse_stream_parses_buffered_chunks_when_env_is_unavailable. The stream saves no headers and does not stop early. SSE chunks collect inbuffer. The client reads the stream to the end, as it does today. It still reads the session id from the finished response.on_datasupport, such as the Faraday test adapter, behave the same way.await_response_after_disconnectcontrolsabortableitself. The standalone GET listening stream is also not changed. In a reconnection, headers from the first POST take precedence over headers from the replay GET. The initialize response issues the session.Reason for the original exception
The exception came with the incremental SSE work in cb0eb38 ("Support SSE Reconnection Per SEP-1699 in the HTTP Client Transport"). The concern was valid. An early stop discards the
Faraday::Response. At that time, the response was the only source ofMcp-Session-Id. This change moves header capture to an earlier point. Theon_datacallback already reads these headers. The success path no longer needs the finished response object. The error path still waits for it. The guard for status 400 or higher keeps that behavior.No public API changes.
capture_session_infois private.send_requestis its only caller.Tests
Both new tests use a real local TCP server. They follow
test/mcp/client/oauth/bounded_body_test.rb. WebMock and the Faraday test adapter deliver complete bodies at one time. They cannot show the state "response sent, stream still open."test_initialize_returns_promptly_and_captures_session_id_when_sse_stream_stays_open: the server sends the initialize result event withMcp-Session-Id, then keeps the stream open for 5 s. The test checks three points. The client returns in under 2.5 s. It saves the session id and protocol version. It sends the session id on the next request. Onmain, the test fails at 5.01 s. With this change, it passes in well under one second.test_initialize_retries_through_oauth_when_sse_answer_is_401:initializefirst receives a 401. The SSE body holds a JSON-RPC error event. After the OAuth retry, the server sends a 200 SSE answer. The test checks that the OAuth flow ran one time. It also checks that the client saved the session from the retried response. The test passes onmainand with this change. It fails if the guard for status 400 or higher is removed.