Subscriptions¶
listen() opens a subscriptions/listen stream and routes every notification the server tags with that
stream's ID to the given callback. It returns as soon as the request is away, so the caller is not blocked for the
life of the stream.
use Nexus\Mcp\Core\Schema\JsonRpc\JsonRpcNotification;
use Nexus\Mcp\Core\Schema\SubscriptionFilter;
$stream = $client->listen(
new SubscriptionFilter(toolsListChanged: true, resourcesListChanged: true),
static function (JsonRpcNotification $notification): void {
// Only what this stream asked for arrives here.
},
);
$stream->close();
The filter names what the stream wants. The server honours the intersection of the requested set and what it supports, and MUST NOT push a type that was not asked for.
Routing¶
Routing is per stream, and the most specific route wins. A notification that carries a subscription ID goes to
that stream's callback and to nothing else. One that arrives untagged, or names a stream this client does not
hold, falls through to the build-time notification handler for its method. This mirrors how a per-call
onProgress claims its token ahead of the build-time notifications/progress handler.
Closing and awaiting¶
close() ends the stream. It sends the notifications/cancelled the spec requires and retires the correlation
slot. It is idempotent, and the server answers an abrupt close with nothing.
await() blocks until the server tears the stream down. It returns the empty result the spec calls graceful
closure. A stream the client closed carries no response, so do not await one.
No deadline applies. A listen request legitimately never returns, so it is exempt from the request timeouts that bound every other call.
What ends a stream¶
A delivery shed at the in-flight dispatch cap ends the stream. A lost
delivery cannot be detected from the ones that follow it, so the client fails the stream rather than leave it
silently stale. await() throws SubscriptionDeliveryDroppedException, and listen() again resubscribes.
A restart does not spend the stream. Behind a SupervisedTransport, the client re-sends an open stream to each
replacement peer under the same subscription ID, so the callback keeps firing and await() resumes rather than
settles. A server that refuses the subscription has answered it, so that still ends the stream on any
transport. See Subscriptions across a restart for
the full list of what ends a stream for good.
examples/subscriptions.php runs both halves in one process: a filtered
stream, a runtime tool addition, and a published resource update.