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;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Client\Dispatch\ClientInitializationGate;
18: use Nexus\Mcp\Client\Dispatch\ClientMessageDispatcher;
19: use Nexus\Mcp\Client\Dispatch\ProgressListenerRegistry;
20: use Nexus\Mcp\Client\Handler\Notification\RoutingProgressNotificationHandler;
21: use Nexus\Mcp\Core\Dispatch\PendingOutboundRequests;
22: use Nexus\Mcp\Core\Handler\HandlerRegistry;
23: use Nexus\Mcp\Core\Handler\NotificationHandlerInterface;
24: use Nexus\Mcp\Core\Handler\Request\PingRequestHandler;
25: use Nexus\Mcp\Core\Handler\RequestHandlerInterface;
26: use Nexus\Mcp\Core\Schema\Icon;
27: use Nexus\Mcp\Core\Schema\Implementation;
28: use Nexus\Mcp\Core\Schema\Notification\ProgressNotification;
29: use Nexus\Mcp\Core\Schema\Request\PingRequest;
30: use Nexus\Mcp\Core\Schema\Result;
31: use Psr\Log\LoggerInterface;
32: use Psr\Log\NullLogger;
33:
34: /**
35: * Fluent builder that assembles the per-feature handler registries, the
36: * client-side dispatch kernel, the outbound-request correlator, and the
37: * handshake gate into a runnable `Client` instance.
38: */
39: final class ClientBuilder
40: {
41: private ?Implementation $clientInfo = null;
42: private LoggerInterface $logger;
43:
44: /**
45: * @var array<non-empty-string, RequestHandlerInterface<non-empty-string, Result, ClientContext>>
46: */
47: private array $requestHandlers = [];
48:
49: /**
50: * @var array<non-empty-string, NotificationHandlerInterface<non-empty-string>>
51: */
52: private array $notificationHandlers = [];
53:
54: /**
55: * @var null|\Closure(): (int|non-empty-string)
56: */
57: private ?\Closure $requestIdFactory = null;
58:
59: /**
60: * @var null|\Closure(): (int|non-empty-string)
61: */
62: private ?\Closure $progressTokenFactory = null;
63:
64: public function __construct()
65: {
66: $this->logger = new NullLogger();
67: }
68:
69: /**
70: * @param null|list<Icon> $icons
71: */
72: public function setClientInfo(
73: string $name,
74: string $version,
75: ?string $title = null,
76: ?string $description = null,
77: ?string $websiteUrl = null,
78: ?array $icons = null,
79: ): self {
80: $this->clientInfo = new Implementation($name, $version, $title, $description, $websiteUrl, $icons);
81:
82: return $this;
83: }
84:
85: public function setLogger(LoggerInterface $logger): self
86: {
87: $this->logger = $logger;
88:
89: return $this;
90: }
91:
92: /**
93: * Overrides the default monotonically-incrementing integer factory.
94: *
95: * @param \Closure(): (int|non-empty-string) $factory
96: */
97: public function setRequestIdFactory(\Closure $factory): self
98: {
99: $this->requestIdFactory = $factory;
100:
101: return $this;
102: }
103:
104: /**
105: * Overrides the default progress-token factory used by `Client::callTool()`
106: * when an `onProgress` callback is supplied.
107: *
108: * @param \Closure(): (int|non-empty-string) $factory
109: */
110: public function setProgressTokenFactory(\Closure $factory): self
111: {
112: $this->progressTokenFactory = $factory;
113:
114: return $this;
115: }
116:
117: /**
118: * Registers a handler for an inbound request method the peer may send to the client.
119: *
120: * @param non-empty-string $method
121: * @param RequestHandlerInterface<non-empty-string, Result, ClientContext> $handler
122: */
123: public function addRequestHandler(string $method, RequestHandlerInterface $handler): self
124: {
125: $this->requestHandlers[$method] = $handler;
126:
127: return $this;
128: }
129:
130: /**
131: * Registers a handler for an inbound notification method.
132: *
133: * @param non-empty-string $method
134: * @param NotificationHandlerInterface<non-empty-string> $handler
135: */
136: public function addNotificationHandler(string $method, NotificationHandlerInterface $handler): self
137: {
138: $this->notificationHandlers[$method] = $handler;
139:
140: return $this;
141: }
142:
143: public function build(): Client
144: {
145: Assert::that($this->clientInfo)->isInstanceOf(
146: Implementation::class,
147: 'Client information must be set before build() via setClientInfo().',
148: );
149:
150: $outboundRequests = new PendingOutboundRequests();
151: $progressListeners = new ProgressListenerRegistry();
152:
153: $requestHandlers = $this->requestHandlers;
154: $requestHandlers[PingRequest::getMethod()] ??= new PingRequestHandler();
155:
156: $notificationHandlers = $this->notificationHandlers;
157: $notificationHandlers[ProgressNotification::getMethod()] = new RoutingProgressNotificationHandler(
158: $progressListeners,
159: // register the custom progress handler as fallback
160: $notificationHandlers[ProgressNotification::getMethod()] ?? null,
161: );
162:
163: return new Client(
164: $this->clientInfo,
165: new ClientMessageDispatcher(
166: new HandlerRegistry($requestHandlers, RequestHandlerInterface::class, 'Request handler'),
167: new HandlerRegistry($notificationHandlers, NotificationHandlerInterface::class, 'Notification handler'),
168: $outboundRequests,
169: logger: $this->logger,
170: ),
171: $outboundRequests,
172: new ClientInitializationGate(),
173: $this->requestIdFactory ?? self::buildDefaultRequestIdFactory(),
174: $this->progressTokenFactory ?? self::buildDefaultProgressTokenFactory(),
175: $progressListeners,
176: $this->logger,
177: );
178: }
179:
180: /**
181: * @return \Closure(): int
182: */
183: private static function buildDefaultRequestIdFactory(): \Closure
184: {
185: $counter = 0;
186:
187: return static function () use (&$counter): int {
188: return ++$counter;
189: };
190: }
191:
192: /**
193: * @return \Closure(): non-empty-string
194: */
195: private static function buildDefaultProgressTokenFactory(): \Closure
196: {
197: $counter = 0;
198:
199: return static function () use (&$counter): string {
200: return \sprintf('progress-%d', ++$counter);
201: };
202: }
203: }
204: