Skip to content

Support server→client streaming (subscriptions/listen fan-out) over stateless Streamable HTTP #1662

@halter73

Description

@halter73

Background

PR #1610 (default draft protocol: sessionless + handshake-less, SEP-2575) implements the subscriptions/listen list-changed fan-out on the server (McpServerImpl.SendListChangedNotificationAsync). It works end-to-end over stdio (the only stateful transport after that PR) but not over stateless Streamable HTTP, which is the new default.

Problem

In stateless Streamable HTTP each POST is handled as a self-contained request→response with no long-lived server→client channel (the GET stream is not mapped in stateless mode, and StreamableHttpServerTransport { Stateless = true } drops unsolicited notifications). As a result:

  • A subscriptions/listen POST cannot stay open to receive the SEP-2575 acknowledgement or subsequent */list_changed notifications — they are routed to a transport that discards them.
  • SDK-generated list-changed notifications (raised by the tool/prompt/resource collection Changed events) therefore never reach a stateless HTTP client, even when it has an open subscription.

This is a transport/hosting-layer gap, not a gap in the fan-out logic itself. Note the SDK has never supported unsolicited server→client notifications in stateless mode; this is net-new capability, not a regression.

Why the current design doesn't extend to stateless HTTP

The fan-out shipped in #1610 works by:

  1. Tracking every active subscription in a per-McpServerImpl dictionary (_activeSubscriptions).
  2. Subscribing to the shared tool/prompt/resource collection Changed events.
  3. On a change, broadcasting to each matching subscription over its captured RelatedTransport.

This relies on a single, long-lived McpServerImpl instance owning both the subscriptions and the collection-change source. That holds for stdio (one process, one server instance), but in stateless HTTP each request can be served by a different (effectively per-request) server context, so there is no shared instance to hold _activeSubscriptions or to receive the Changed broadcast. Generalizing the current approach would require persisting subscription state at a new hosting layer plus a global cross-request broadcast API — a large, invasive change.

Design direction under consideration (preferred)

Rather than sharing server state and broadcasting unsolicited notifications, treat the subscriptions/listen request as a held-open, solicited channel:

  • The subscriptions/listen handler keeps the POST response stream open and, for its lifetime, observes the relevant collection changes itself, emitting the acknowledgement and subsequent */list_changed notifications as solicited messages routed over that request's RelatedTransport / StreamableHttpPostTransport.
  • Because everything is scoped to the in-flight listen request, there is no need for a shared McpServerImpl instance, a cross-request _activeSubscriptions registry, or a global broadcast API. This mirrors how MRTR / elicitation already deliver solicited server→client messages over RelatedTransport in stateless mode.
  • The existing collection Changed events must keep firing automatically so server authors don't have to do anything; the listen handler is just another consumer of those events for the duration of the open stream.

This is a major rearchitecture and is intentionally out of scope for #1610 — that PR ships the spec-correct stdio behavior and explicitly punts the stateless HTTP path here.

Alternative (heavier)

Keep the broadcast model but lift subscription state into the hosting layer (StreamableHttpHandler) with a new "durable server→client stream" abstraction and a global broadcast API. Considered less desirable because it leaks transport/hosting concerns and reintroduces shared mutable state across requests; captured here for completeness.

Out of scope / related

Acceptance criteria

  • A stateless Streamable HTTP client can issue subscriptions/listen, receive the tagged acknowledgement, and receive subsequent SDK-generated */list_changed notifications (tagged with the subscription id) over the open POST stream.
  • SDK-generated list-changed notifications continue to fire automatically from collection changes, with no API change required of server authors.
  • No reliance on a shared cross-request McpServerImpl instance for delivery; layering is preserved (no transport internals exposed through the Core server API).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions