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\Core\Schema\Result;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Arrayable;
18: use Nexus\Mcp\Core\Schema\Cursor;
19: use Nexus\Mcp\Core\Schema\Enum\CacheScope;
20: use Nexus\Mcp\Core\Schema\Enum\ResultType;
21: use Nexus\Mcp\Core\Schema\MetaObject;
22: use Nexus\Mcp\Core\Schema\MetaObject\GenericResultMetaObject;
23: use Nexus\Mcp\Core\Schema\MetaObject\ResultMetaObject;
24: use Nexus\Mcp\Core\Schema\Tool\Tool;
25:
26: /**
27: * The result returned by the server for a `tools/list` request.
28: *
29: * @extends PaginatedResult<array{
30: * _meta?: template-type<ResultMetaObject, MetaObject, 'T'>,
31: * resultType: non-empty-string,
32: * tools: list<template-type<Tool, Arrayable, 'T'>>,
33: * nextCursor?: non-empty-string,
34: * ttlMs: int,
35: * cacheScope: value-of<CacheScope>,
36: * }>
37: *
38: * @see https://modelcontextprotocol.io/specification/2026-07-28/schema#listtoolsresult
39: */
40: final readonly class ListToolsResult extends PaginatedResult implements ServerResult
41: {
42: /**
43: * @param list<Tool> $tools
44: */
45: public function __construct(
46: public array $tools,
47: int $ttlMs,
48: CacheScope $cacheScope,
49: ?Cursor $nextCursor = null,
50: ResultMetaObject $meta = new GenericResultMetaObject(),
51: ) {
52: Assert::that($tools)
53: ->isList('"result.tools" must be a list, non-list array given.')
54: ->values()->isInstanceOf(Tool::class)
55: ;
56:
57: parent::__construct(ttlMs: $ttlMs, cacheScope: $cacheScope, nextCursor: $nextCursor, meta: $meta);
58: }
59:
60: #[\Override]
61: public static function fromArray(array $data): static
62: {
63: Assert::that($data)->hasOffset('tools', '"result" is missing the required "tools" key.');
64: Assert::that($data['tools'])
65: ->isList('"result.tools" must be a list, {type} given.')
66: ->values()
67: ->isArray('each "result.tool" must be an object, {type} given.')
68: ->isMap('each "result.tool" must be a string-keyed object.')
69: ;
70: $tools = array_map(Tool::fromArray(...), $data['tools']);
71:
72: Assert::that($data)->hasOffset('ttlMs', '"result" is missing the required "ttlMs" key.');
73: $ttlMs = $data['ttlMs'];
74: Assert::that($ttlMs)->isInt('"result.ttlMs" must be an integer, {type} given.');
75:
76: Assert::that($data)->hasOffset('cacheScope', '"result" is missing the required "cacheScope" key.');
77: Assert::that($data['cacheScope'])->isOneOf(array_column(CacheScope::cases(), 'value'), '"result.cacheScope" must be one of {choices}, {value} given.');
78: $cacheScope = CacheScope::from($data['cacheScope']);
79:
80: $nextCursor = null;
81:
82: if (\array_key_exists('nextCursor', $data)) {
83: $raw = $data['nextCursor'];
84: Assert::that($raw)->isNonEmptyString('"result.nextCursor" must be a non-empty string, {type} given.');
85: $nextCursor = new Cursor(cursor: $raw);
86: }
87:
88: $meta = new GenericResultMetaObject();
89:
90: if (\array_key_exists('_meta', $data)) {
91: Assert::that($data['_meta'])
92: ->isArray('"result._meta" must be an object, {type} given.')
93: ->not()->isNonEmptyList('"result._meta" must be a string-keyed object.')
94: ;
95: $meta = GenericResultMetaObject::fromArray($data['_meta']);
96: }
97:
98: return new self(tools: $tools, ttlMs: $ttlMs, cacheScope: $cacheScope, nextCursor: $nextCursor, meta: $meta);
99: }
100:
101: #[\Override]
102: public function toArray(): array
103: {
104: $data = [];
105: $meta = $this->meta->toArray();
106:
107: if ([] !== $meta) {
108: $data['_meta'] = $meta;
109: }
110:
111: $data['resultType'] = self::getResultType();
112: $data['tools'] = array_map(static fn(Tool $tool): array => $tool->toArray(), $this->tools);
113:
114: if (null !== $this->nextCursor) {
115: $data['nextCursor'] = $this->nextCursor->cursor;
116: }
117:
118: $data['ttlMs'] = $this->ttlMs;
119: $data['cacheScope'] = $this->cacheScope->value;
120:
121: return $data;
122: }
123:
124: #[\Override]
125: public function jsonSerialize(): array
126: {
127: $data = $this->toArray();
128: $data['tools'] = array_map(static fn(Tool $tool): array => $tool->jsonSerialize(), $this->tools);
129:
130: return $data;
131: }
132:
133: #[\Override]
134: public function rebuildWithMeta(ResultMetaObject $meta): static
135: {
136: return new self(
137: tools: $this->tools,
138: ttlMs: $this->ttlMs,
139: cacheScope: $this->cacheScope,
140: nextCursor: $this->nextCursor,
141: meta: $meta,
142: );
143: }
144:
145: #[\Override]
146: protected function getResultType(): string
147: {
148: return ResultType::Complete->value;
149: }
150: }
151: