stacklok / stacklok/toolhive

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

Open
#6,065 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage
Dominant language
Go
Stars
2.2k
Forks
300
Avg merge
1d 15h
Merged PRs (30d)
184

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with modern_subscriptions.go, newModernCapabilities, and pkg/vmcp/session/factory.go; compare their current behavior with schema/draft/schema.ts:120-133 and 1354-1363. Review buildListChangedSink, the negative capability-regression test, and docs/arch/03-transport-architecture.md. Done means supported subscriptions deliver conformant notifications, authorization and resource limits are addressed, and coverage restores the DiscoverCapabilities paths and non-empty branch.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, typescript
Domain
api, backend, distributed-systems
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.