Skip to content

Modern push delivery: honor subscriptions and deliver notifications over subscriptions/listen #6065

Description

@JAORMX

Summary

#6050 implements subscriptions/listen as a capability-negative handler: it acknowledges the client's requested subscription set intersected against vMCP's advertised capabilities, and since vMCP advertises all four push capabilities as false, the honored set is always empty and the stream closes immediately.

That is conformant and deliberate — the specification's acknowledgement is the designated mechanism for declining, and schema/draft/schema.ts:1354-1363 says the honored set "only includes notification types the server actually supports; if the client requested an unsupported type … it is omitted from this set." But it means vMCP has nothing to push. This issue tracks making delivery real.

What has to change

1. Start advertising the capabilities. newModernCapabilities builds zero-value structs; Tools.ListChanged, Prompts.ListChanged, Resources.ListChanged and Resources.Subscribe are never set true anywhere in the tree, and no config field, env var, CRD field or YAML key reaches them. Flipping any of them is a source change.

2. Build per-subscription backend fan-in. The delivery source does not exist on the Modern path:

  • Resource subscriptions are acknowledgement-level only — vMCP records resources/subscribe but never forwards a backend's notifications/resources/updated.
  • list_changed propagation depends on a persistent per-backend connection that pkg/vmcp/session/factory.go deliberately does not open for Modern backends.
  • buildListChangedSink has a single production call site, inside the SDK ClientSession registration hook. dispatchModern creates no session, so for a Modern client the sink is never built at all, regardless of backend revision.

3. Honour the delivery-side MUST. schema/draft/schema.ts:120-133 requires the io.modelcontextprotocol/subscriptionId key on every notification delivered via a subscriptions/listen stream, not just the acknowledgement. A correct acknowledgement followed by a bare notifications/tools/list_changed is non-conformant. This is recorded at the hand-off WARN in modern_subscriptions.go.

4. Decide whether serverStreamRegistry is the fan-out seam. #6050 deliberately did not reuse it — with an empty honored set there is no fan-out, so adapting an unexported registry from pkg/transport/proxy/streamable would have added an abstraction with zero consumers. docs/arch/03-transport-architecture.md now frames it as the seam for delivery rather than for the handler, so that question becomes live here.

Coverage that must be restored at the same time

Three code paths are currently unreachable because the honored set is always empty, and #6050 removed or omitted tests for them rather than assert on unenterable branches. Whoever flips a capability flag re-enables all three and must restore coverage:

  • the DiscoverCapabilities call itself (skipped today by a ceiling pre-check, so the fan-out is not paid for a result that would be discarded)
  • that call's error path, whose test was removed
  • the non-empty-honored branch of the handler

This is recorded in the negative capability-regression test's comment, which asserts all four flags are false precisely so that a flip cannot happen silently.

Note on DoS

The pre-check exists because subscriptions/listen otherwise performs an un-metered backend fan-out whose result is provably always discarded. Once delivery is real, a long-lived POST per subscription becomes a genuine resource-consumption surface — concurrent stream caps and per-subscription accounting want designing in, not bolting on. Also note resourceSubscriptions URIs arrive from the client and are currently echoed back unfiltered; per-URI authorization needs to apply before any subscription is honored.

Refs #5743, #6050.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageIssue needs initial triage by a maintainer

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions