Keep the acknowledgement first when the transport closes during a listen registration - #574
Open
koic wants to merge 1 commit into
Conversation
…ten registration ## Motivation and Context A `subscriptions/listen` stream is registered before its acknowledgement is written, and the acknowledgement was written outside any lock. Closing the transport snapshots the registry, then writes each stream's `SubscriptionsListenResult` under the stream's write mutex and closes it. When the close landed between the registration and the acknowledgement write, the client received the result and the stream closed; the acknowledgement write then failed against the closed stream. SEP-2575 requires the acknowledgement to be the first message on the stream, so such a stream ended in a shape the specification does not allow. The acknowledgement is now written under the same write mutex, and it is skipped when the transport has already marked the entry closed. Closing the transport writes the result only to streams that were acknowledged; a stream that never was is closed without one, an abrupt close the specification allows and the client answers by sending `subscriptions/listen` again. Since both sides serialize on the one mutex, whenever both messages are written the acknowledgement precedes the result, and no result reaches a stream that received no acknowledgement. The flag that marks a stream acknowledged is set under the registry lock as well as the write mutex, since the registry lock is the one a notification's delivery snapshot reads it under; nothing holds the registry lock while waiting for a write mutex, so the two cannot deadlock. ## How Has This Been Tested? New tests in `test/mcp/server/transports/streamable_http_transport_test.rb` close the transport while an acknowledgement write is in progress and check that the stream carries the acknowledgement and then the result, and close it with a registered but unacknowledged stream and check that no result is written. Against the previous library the first stream carries only the result. ## Breaking Changes None. A listen stream that the transport closes before its acknowledgement was written now ends without a result instead of receiving one first.
atesgoral
approved these changes
Sep 23, 2026
This branch has not been deployed
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
A
subscriptions/listenstream is registered before its acknowledgement is written, and the acknowledgement was written outside any lock. Closing the transport snapshots the registry, then writes each stream'sSubscriptionsListenResultunder the stream's write mutex and closes it. When the close landed between the registration and the acknowledgement write, the client received the result and the stream closed; the acknowledgement write then failed against the closed stream. SEP-2575 requires the acknowledgement to be the first message on the stream, so such a stream ended in a shape the specification does not allow.The acknowledgement is now written under the same write mutex, and it is skipped when the transport has already marked the entry closed. Closing the transport writes the result only to streams that were acknowledged; a stream that never was is closed without one, an abrupt close the specification allows and the client answers by sending
subscriptions/listenagain. Since both sides serialize on the one mutex, whenever both messages are written the acknowledgement precedes the result, and no result reaches a stream that received no acknowledgement.The flag that marks a stream acknowledged is set under the registry lock as well as the write mutex, since the registry lock is the one a notification's delivery snapshot reads it under; nothing holds the registry lock while waiting for a write mutex, so the two cannot deadlock.
How Has This Been Tested?
New tests in
test/mcp/server/transports/streamable_http_transport_test.rbclose the transport while an acknowledgement write is in progress and check that the stream carries the acknowledgement and then the result, and close it with a registered but unacknowledged stream and check that no result is written. Against the previous library the first stream carries only the result.Breaking Changes
None. A listen stream that the transport closes before its acknowledgement was written now ends without a result instead of receiving one first.
Types of changes
Checklist