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