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.
Summary
#6050 implements
subscriptions/listenas 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 asfalse, 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-1363says 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.
newModernCapabilitiesbuilds zero-value structs;Tools.ListChanged,Prompts.ListChanged,Resources.ListChangedandResources.Subscribeare 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:
resources/subscribebut never forwards a backend'snotifications/resources/updated.list_changedpropagation depends on a persistent per-backend connection thatpkg/vmcp/session/factory.godeliberately does not open for Modern backends.buildListChangedSinkhas a single production call site, inside the SDKClientSessionregistration hook.dispatchModerncreates 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-133requires theio.modelcontextprotocol/subscriptionIdkey on every notification delivered via asubscriptions/listenstream, not just the acknowledgement. A correct acknowledgement followed by a barenotifications/tools/list_changedis non-conformant. This is recorded at the hand-offWARNinmodern_subscriptions.go.4. Decide whether
serverStreamRegistryis 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 frompkg/transport/proxy/streamablewould have added an abstraction with zero consumers.docs/arch/03-transport-architecture.mdnow 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:
DiscoverCapabilitiescall itself (skipped today by a ceiling pre-check, so the fan-out is not paid for a result that would be discarded)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/listenotherwise 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 noteresourceSubscriptionsURIs arrive from the client and are currently echoed back unfiltered; per-URI authorization needs to apply before any subscription is honored.Refs #5743, #6050.