Skip to content

Add public subscriptions/listen server handler (SEP-2575) - #1775

Open
tarekgh wants to merge 3 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/subscriptions-listen-handler
Open

Add public subscriptions/listen server handler (SEP-2575)#1775
tarekgh wants to merge 3 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/subscriptions-listen-handler

Conversation

@tarekgh

@tarekgh tarekgh commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #1662

Adds an opt-in, typed subscriptions/listen server handler so server authors can own the long-lived subscription stream directly.

What

  • McpServerHandlers.SubscriptionsListenHandler handler slot.
  • WithSubscriptionsListenHandler(...) builder method.

When set, the handler is a full replacement for the built-in subscriptions/listen handling: it owns the stream, sends the acknowledgement, tags notifications with the subscription id, and receives no automatic */list_changed fan-out. It runs through the same request pipeline as other typed handlers, so it can stream notifications over the request response stream even under stateless Streamable HTTP, where the held-open POST is the only solicited server-to-client channel. Protocol-version gating (2026-07-28+) is enforced by the SDK. No server capabilities are auto-advertised, so authors declare only what their handler delivers.

Tests

Acknowledgement-first ordering and subscription-id tagging, replacement suppressing automatic list-changed fan-out, pre-2026-07-28 rejection, cancellation cleanup, and stateless streaming over the held-open POST. Full core suite passes.

Tarek Mahmoud Sayed added 2 commits July 29, 2026 14:18
Adds an opt-in McpServerHandlers.SubscriptionsListenHandler and a
WithSubscriptionsListenHandler builder method so server authors can own
the long-lived subscriptions/listen stream for custom subscription kinds,
application-driven notifications, and stateless Streamable HTTP delivery
over the held-open POST response.

Fixes modelcontextprotocol#1662

Copilot-Session: 80653343-5dcb-43cb-89b0-8ac8e572c4f7
The client dispatches incoming messages concurrently, so cross-notification
arrival order is not observable at the handler level. Assert only that the
acknowledgement and streamed notification each arrive tagged with the
subscription id, and prove fan-out suppression via the synchronous
empty-fan-out plus a completed-and-empty channel check.

Copilot-Session: 80653343-5dcb-43cb-89b0-8ac8e572c4f7
@tarekgh

tarekgh commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

CC @jeffhandley

Comment thread src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Comment thread tests/ModelContextProtocol.Tests/Server/SubscriptionsListenHandlerTests.cs Outdated
Comment thread src/ModelContextProtocol.Core/Server/McpServerImpl.cs
- Normalize a null Notifications to empty in the custom-handler wrapper so
  a '{"notifications": null}' payload no longer NREs into a generic
  InternalError.
- Decide the listChanged capability per response instead of clearing it in
  the constructor: initialize never advertises it for stateless servers,
  while server/discover advertises it when a custom SubscriptionsListenHandler
  can deliver it over the listen stream.
- Add WithSubscriptionsListenHandler_Sets_Handler builder test.
- Add stateless test covering listChanged advertisement and delivery over the
  held-open POST.
- Remove the unnecessary McpServerToolType attribute from the ListenTools test
  type registered via WithTools<T>().
@tarekgh

tarekgh commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@halter73 all four review comments are addressed in cff1165:

  1. The custom-handler wrapper normalizes a null Notifications to empty before calling the handler.
  2. Added WithSubscriptionsListenHandler_Sets_Handler.
  3. Removed the unnecessary McpServerToolType attribute from the ListenTools test type.
  4. The listChanged capability is now decided per response (GetAdvertisedCapabilities) rather than in the constructor, so server/discover advertises it when a custom handler can deliver it while initialize does not, plus a new stateless test covering listChanged advertisement and delivery.

Full core and AspNetCore test suites pass. Ready for another look.

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.

Add a public subscriptions/listen server handler for custom streaming

2 participants