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\Exception;
15:
16: use Nexus\Mcp\Core\Exception\AbstractJsonRpcProtocolException;
17: use Nexus\Mcp\Core\Schema\ClientCapabilities;
18: use Nexus\Mcp\Core\Schema\Enum\ProtocolErrorCode;
19: use Nexus\Mcp\Core\Schema\RequestId;
20:
21: /**
22: * Thrown when serving a request would rely on a client capability absent from the request's
23: * `io.modelcontextprotocol/clientCapabilities`.
24: */
25: final class MissingRequiredClientCapabilityException extends AbstractJsonRpcProtocolException
26: {
27: public function __construct(
28: public readonly ClientCapabilities $requiredCapabilities,
29: ?RequestId $requestId = null,
30: ?\Throwable $previous = null,
31: ) {
32: parent::__construct(
33: $requestId,
34: \sprintf(
35: 'This request requires client capabilities the client did not declare: %s.',
36: implode(', ', $this->describeCapabilities($requiredCapabilities->toArray())),
37: ),
38: $previous,
39: errorData: ['requiredCapabilities' => $requiredCapabilities->toArray()],
40: );
41: }
42:
43: #[\Override]
44: public static function getErrorCode(): ProtocolErrorCode
45: {
46: return ProtocolErrorCode::MissingRequiredClientCapability;
47: }
48:
49: /**
50: * Renders each required slot, naming the nested members of a map-valued slot
51: * (`extensions.com.example/feature`).
52: *
53: * @param array<string, mixed> $capabilities
54: *
55: * @return list<string>
56: */
57: private function describeCapabilities(array $capabilities): array
58: {
59: $described = [];
60:
61: foreach ($capabilities as $slot => $members) {
62: if (\is_array($members) && [] !== $members && $this->hasOnlyStringKeys($members)) {
63: foreach (array_keys($members) as $member) {
64: $described[] = \sprintf('%s.%s', $slot, $member);
65: }
66:
67: continue;
68: }
69:
70: $described[] = $slot;
71: }
72:
73: return $described;
74: }
75:
76: /**
77: * Whether every key names a member, which is what separates a map-valued slot from a list.
78: *
79: * @param non-empty-array<array-key, mixed> $members
80: */
81: private function hasOnlyStringKeys(array $members): bool
82: {
83: foreach (array_keys($members) as $key) {
84: if (! \is_string($key)) {
85: return false;
86: }
87: }
88:
89: return true;
90: }
91: }
92: