1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus MCP SDK package.
7: *
8: * (c) 2026 John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\Mcp\Client\Subscription;
15:
16: use Amp\Future;
17: use Nexus\Mcp\Client\Exception\SubscriptionClosedException;
18: use Nexus\Mcp\Core\Exception\RemoteCallFailedException;
19: use Nexus\Mcp\Core\Schema\RequestId;
20: use Nexus\Mcp\Core\Schema\Result\SubscriptionsListenResult;
21:
22: /**
23: * An open `subscriptions/listen` stream. Ends when the caller closes it or the server tears it down.
24: * A supervised transport re-opens it against each replacement peer under the same subscription id,
25: * so neither the id nor this object is spent by a restart.
26: */
27: final class SubscriptionStream
28: {
29: private bool $closed = false;
30:
31: /**
32: * @param Future<SubscriptionsListenResult> $outcome
33: * @param \Closure(): void $onClose
34: *
35: * @internal
36: */
37: public function __construct(
38: public readonly RequestId $subscriptionId,
39: private readonly Future $outcome,
40: private readonly \Closure $onClose,
41: ) {
42: }
43:
44: /**
45: * Stops the stream, telling the server the subscription is over. Subsequent calls are no-ops.
46: */
47: public function close(): void
48: {
49: if ($this->closed) {
50: return;
51: }
52:
53: $this->closed = true;
54: ($this->onClose)();
55: }
56:
57: /**
58: * Blocks until the server tears the subscription down of its own accord. A peer that dies under
59: * supervision does not settle this: the wait resumes against the replacement.
60: *
61: * @throws RemoteCallFailedException
62: * @throws SubscriptionClosedException
63: */
64: public function await(): SubscriptionsListenResult
65: {
66: if ($this->closed) {
67: // The spec answers an abrupt close with nothing, so this response can never arrive and
68: // awaiting it would park the caller for good.
69: throw new SubscriptionClosedException($this->subscriptionId);
70: }
71:
72: return $this->outcome->await();
73: }
74: }
75: