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\Server;
15:
16: use Amp\DeferredFuture;
17: use Nexus\Mcp\Core\Dispatch\MessageDispatcherInterface;
18: use Nexus\Mcp\Core\Schema\RequestId;
19: use Nexus\Mcp\Core\Transport\CancellableTransportInterface;
20: use Nexus\Mcp\Core\Transport\ReceiveContext;
21: use Nexus\Mcp\Core\Transport\TransportInterface;
22: use Nexus\Mcp\Server\Subscription\SubscriptionStoreInterface;
23: use Psr\Log\LoggerInterface;
24: use Psr\Log\NullLogger;
25:
26: /**
27: * Thin shell that drives a single transport's lifecycle.
28: */
29: final readonly class Server
30: {
31: public function __construct(
32: private MessageDispatcherInterface $dispatcher,
33: private LoggerInterface $logger = new NullLogger(),
34: private ?SubscriptionStoreInterface $subscriptions = null,
35: ) {
36: }
37:
38: /**
39: * Runs the server on the transport, blocking until it closes, for a long-lived
40: * transport that owns its read loop (stdio).
41: */
42: public function run(TransportInterface $transport): void
43: {
44: $this->logger->info('Starting MCP server.');
45:
46: $deferred = new DeferredFuture();
47:
48: $this->attachDispatchListeners($transport);
49:
50: $transport->onClose(static function () use ($deferred): void {
51: if ($deferred->isComplete()) {
52: return;
53: }
54:
55: $deferred->complete();
56: });
57:
58: $transport->start();
59: $deferred->getFuture()->await();
60:
61: $this->logger->info('MCP server stopped.');
62: }
63:
64: /**
65: * Attaches the dispatcher and starts the transport without blocking, for a
66: * request-scoped transport (streamable HTTP in a PSR-15 stack) the host drives per request.
67: */
68: public function listen(TransportInterface $transport): void
69: {
70: $this->attachDispatchListeners($transport);
71:
72: $transport->start();
73: }
74:
75: private function attachDispatchListeners(TransportInterface $transport): void
76: {
77: $this->subscriptions?->reopen();
78:
79: $transport->onMessage(function (array $envelope, ReceiveContext $context) use ($transport): void {
80: $this->dispatcher->dispatch($envelope, $transport, $context);
81: });
82: $transport->onError(function (\Throwable $e): void {
83: $this->logger->error('Transport error.', ['exception' => $e]);
84: });
85: $transport->onDrain(function (): void {
86: $this->subscriptions?->closeAll();
87: $this->dispatcher->flushPending();
88: });
89:
90: if ($transport instanceof CancellableTransportInterface) {
91: $transport->onCancel(function (RequestId $id): void {
92: $this->dispatcher->cancelRequest($id);
93: });
94: }
95: }
96: }
97: