Skip to content

Serve subscriptions/listen streams that carry the same request id - #567

Merged
koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:key_listen_streams_by_a_server_minted_identifier
Sep 22, 2026
Merged

koic merged 1 commit into
modelcontextprotocol:mainfrom
koic:key_listen_streams_by_a_server_minted_identifier

Conversation

@koic

@koic koic commented Sep 22, 2026

Copy link
Copy Markdown
Member

Motivation and Context

The Streamable HTTP transport kept its open subscriptions/listen streams in one registry keyed by the request's JSON-RPC id, and closed a new stream before its acknowledgement whenever another stream already carried that id. The specification scopes request id uniqueness to a single sender's in-flight requests, and stamps the id into _meta as subscriptionId so that a client can tell its own streams apart; nothing makes it unique across clients. The TypeScript and Python clients number their requests with a counter, so two of them whose listen request carried the same small integer collided, and the second saw its stream end with neither an acknowledgement nor an error. On the modern lifecycle every client reaches that registry without a session, so one client could also keep another from opening a stream under an id it held. The reference SDKs keep each listen stream scoped to its own connection and hold no id-keyed registry across clients.

The registry is now keyed by an identifier the transport mints for each stream, and the request id is kept on the entry for the subscriptionId in acknowledgements, notifications, and the closing result. Streams that carry the same request id are served side by side, each stamped with the id it sent, and a write failure or the keepalive of one of them frees only that stream. The concurrent stream cap is unchanged.

How Has This Been Tested?

New tests in test/mcp/server/transports/streamable_http_transport_test.rb open two streams under the same request id and check that both are acknowledged, both receive a notification stamped with that id, each receives its own result on transport close, and a failed write on one frees only that one. Against the previous library the second stream is closed before its acknowledgement.

Breaking Changes

None. A subscriptions/listen stream whose request id another open stream already carries is served instead of being closed.

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

## Motivation and Context

The Streamable HTTP transport kept its open `subscriptions/listen` streams in one registry keyed by
the request's JSON-RPC id, and closed a new stream before its acknowledgement whenever another stream
already carried that id. The specification scopes request id uniqueness to a single sender's in-flight
requests, and stamps the id into `_meta` as `subscriptionId` so that a client can tell its own streams
apart; nothing makes it unique across clients. The TypeScript and Python clients number their requests
with a counter, so two of them whose listen request carried the same small integer collided, and the second
saw its stream end with neither an acknowledgement nor an error. On the modern lifecycle every client reaches
that registry without a session, so one client could also keep another from opening a stream under an id
it held. The reference SDKs keep each listen stream scoped to its own connection and hold no id-keyed registry
across clients.

The registry is now keyed by an identifier the transport mints for each stream, and the request id is kept
on the entry for the `subscriptionId` in acknowledgements, notifications, and the closing result. Streams
that carry the same request id are served side by side, each stamped with the id it sent, and a write failure
or the keepalive of one of them frees only that stream. The concurrent stream cap is unchanged.

## How Has This Been Tested?

New tests in `test/mcp/server/transports/streamable_http_transport_test.rb` open two streams under the same
request id and check that both are acknowledged, both receive a notification stamped with that id, each receives
its own result on transport close, and a failed write on one frees only that one. Against the previous library
the second stream is closed before its acknowledgement.

## Breaking Changes

None. A `subscriptions/listen` stream whose request id another open stream already carries is served
instead of being closed.
@koic
koic merged commit 6da3009 into modelcontextprotocol:main Sep 22, 2026
11 checks passed
@koic
koic deleted the key_listen_streams_by_a_server_minted_identifier branch September 22, 2026 16:10
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.

1 participant