You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
subscriptions/listen with an empty agreed filter holds the stream (and a PHP-FPM worker) for the full subscription lifetime #617
In the stateless 2026-07-28 protocol, StatelessProtocol::listen() narrows the requested notification filter to what the server can deliver (NotificationFilter::fromParams(...)->intersect($capabilities)). When that intersection is empty, nothing can ever be delivered on the stream. The server still sends the acknowledgment and then polls every 250 ms until subscriptionLifetime ends (30 s by default).
This happens when:
the client asks for toolsListChanged (or another list type), but the server has no notification bus and therefore does not announce listChanged;
the client asks for resourceSubscriptions, but the server does not announce resources.subscribe;
the client sends an empty notifications object, or none at all.
Under PHP-FPM every one of these streams holds a worker for the full lifetime. A client that reconnects after the graceful closure holds one almost permanently, for a stream that is guaranteed to stay silent. Over stdio the stream is unpaced and stays open until the client cancels it.
Reproduction
$server = Server::builder()
->setServerInfo('test-server', '1.0.0')
->setCapabilities(newServerCapabilities(toolsListChanged: false))
->buildStateless([ProtocolVersion::V2026_07_28]);
// POST subscriptions/listen with params.notifications = {"toolsListChanged": true}
Expected: the acknowledgment with "notifications": {}, then the graceful closure. Actual: the acknowledgment, then about 30 s of polling, then the graceful closure.
Suggested fix
The spec lets the server end a subscription on its own initiative, as long as the acknowledgment comes first, and recommends the resultType: complete response as a graceful closure ("Graceful Closure" in the subscriptions pattern). When the agreed filter carries nothing (no list types, no resource subscriptions), listen() can send the acknowledgment with the empty notifications object followed directly by that closure, without entering the poll loop. The client still learns from the acknowledgment that none of its types are supported, and from the completion that the end was intended rather than a dropped transport.
Activity