Skip to content

Keep the acknowledgement first when the transport closes during a listen registration - #574

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:keep_the_listen_acknowledgement_first_on_transport_close
Open

koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:keep_the_listen_acknowledgement_first_on_transport_close

Conversation

@koic

@koic koic commented Sep 23, 2026

Copy link
Copy Markdown
Member

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.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

…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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants