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\Extension\Tasks\Schema\Result;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Enum\ResultType;
18: use Nexus\Mcp\Core\Schema\MetaObject;
19: use Nexus\Mcp\Core\Schema\MetaObject\GenericResultMetaObject;
20: use Nexus\Mcp\Core\Schema\MetaObject\ResultMetaObject;
21: use Nexus\Mcp\Core\Schema\Result;
22: use Nexus\Mcp\Core\Schema\Result\ServerResult;
23: use Nexus\Mcp\Core\Schema\Result\TaskHandleResult;
24: use Nexus\Mcp\Extension\Tasks\Schema\Enum\TaskStatus;
25:
26: /**
27: * A task handle returned in lieu of a request's standard result, identifying
28: * the long-running task the client polls with `tasks/get`.
29: *
30: * @extends Result<array{
31: * _meta?: template-type<ResultMetaObject, MetaObject, 'T'>,
32: * resultType: non-empty-string,
33: * taskId: non-empty-string,
34: * status: non-empty-string,
35: * createdAt: non-empty-string,
36: * lastUpdatedAt: non-empty-string,
37: * ttlMs: null|int,
38: * statusMessage?: non-empty-string,
39: * pollIntervalMs?: int,
40: * }>
41: *
42: * @see https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md
43: */
44: final readonly class CreateTaskResult extends Result implements ServerResult, TaskHandleResult
45: {
46: /**
47: * @param non-empty-string $taskId
48: * @param non-empty-string $createdAt
49: * @param non-empty-string $lastUpdatedAt
50: * @param null|non-empty-string $statusMessage
51: */
52: public function __construct(
53: public string $taskId,
54: public TaskStatus $status,
55: public string $createdAt,
56: public string $lastUpdatedAt,
57: public ?int $ttlMs,
58: public ?string $statusMessage = null,
59: public ?int $pollIntervalMs = null,
60: ResultMetaObject $meta = new GenericResultMetaObject(),
61: ) {
62: parent::__construct(meta: $meta);
63: }
64:
65: #[\Override]
66: public static function fromArray(array $data): static
67: {
68: Assert::that($data)->hasOffset('taskId', '"result" is missing the required "taskId" key.');
69: $taskId = $data['taskId'];
70: Assert::that($taskId)->isNonEmptyString('"result.taskId" must be a non-empty string, {type} given.');
71:
72: Assert::that($data)->hasOffset('status', '"result" is missing the required "status" key.');
73: Assert::that($data['status'])->isOneOf(array_column(TaskStatus::cases(), 'value'), '"result.status" must be one of {choices}, {value} given.');
74: $status = TaskStatus::from($data['status']);
75:
76: Assert::that($data)->hasOffset('createdAt', '"result" is missing the required "createdAt" key.');
77: $createdAt = $data['createdAt'];
78: Assert::that($createdAt)->isNonEmptyString('"result.createdAt" must be a non-empty string, {type} given.');
79:
80: Assert::that($data)->hasOffset('lastUpdatedAt', '"result" is missing the required "lastUpdatedAt" key.');
81: $lastUpdatedAt = $data['lastUpdatedAt'];
82: Assert::that($lastUpdatedAt)->isNonEmptyString('"result.lastUpdatedAt" must be a non-empty string, {type} given.');
83:
84: Assert::that($data)->hasOffset('ttlMs', '"result" is missing the required "ttlMs" key.');
85: $ttlMs = $data['ttlMs'];
86: Assert::that($ttlMs)->nullOr()->isPositiveInt('"result.ttlMs" must be null or a positive integer, {value} given.');
87:
88: $statusMessage = null;
89:
90: if (\array_key_exists('statusMessage', $data)) {
91: $statusMessage = $data['statusMessage'];
92: Assert::that($statusMessage)->isNonEmptyString('"result.statusMessage" must be a non-empty string, {type} given.');
93: }
94:
95: $pollIntervalMs = null;
96:
97: if (\array_key_exists('pollIntervalMs', $data)) {
98: $pollIntervalMs = $data['pollIntervalMs'];
99: Assert::that($pollIntervalMs)->isPositiveInt('"result.pollIntervalMs" must be a positive integer, {value} given.');
100: }
101:
102: $meta = new GenericResultMetaObject();
103:
104: if (\array_key_exists('_meta', $data)) {
105: Assert::that($data['_meta'])
106: ->isArray('"result._meta" must be an object, {type} given.')
107: ->not()->isNonEmptyList('"result._meta" must be a string-keyed object.')
108: ;
109: $meta = GenericResultMetaObject::fromArray($data['_meta']);
110: }
111:
112: return new self(
113: taskId: $taskId,
114: status: $status,
115: createdAt: $createdAt,
116: lastUpdatedAt: $lastUpdatedAt,
117: ttlMs: $ttlMs,
118: statusMessage: $statusMessage,
119: pollIntervalMs: $pollIntervalMs,
120: meta: $meta,
121: );
122: }
123:
124: #[\Override]
125: public function toArray(): array
126: {
127: $data = [];
128: $meta = $this->meta->toArray();
129:
130: if ([] !== $meta) {
131: $data['_meta'] = $meta;
132: }
133:
134: $data['resultType'] = self::getResultType();
135: $data['taskId'] = $this->taskId;
136: $data['status'] = $this->status->value;
137: $data['createdAt'] = $this->createdAt;
138: $data['lastUpdatedAt'] = $this->lastUpdatedAt;
139: $data['ttlMs'] = $this->ttlMs;
140:
141: if (null !== $this->statusMessage) {
142: $data['statusMessage'] = $this->statusMessage;
143: }
144:
145: if (null !== $this->pollIntervalMs) {
146: $data['pollIntervalMs'] = $this->pollIntervalMs;
147: }
148:
149: return $data;
150: }
151:
152: #[\Override]
153: public function rebuildWithMeta(ResultMetaObject $meta): static
154: {
155: return new self(
156: taskId: $this->taskId,
157: status: $this->status,
158: createdAt: $this->createdAt,
159: lastUpdatedAt: $this->lastUpdatedAt,
160: ttlMs: $this->ttlMs,
161: statusMessage: $this->statusMessage,
162: pollIntervalMs: $this->pollIntervalMs,
163: meta: $meta,
164: );
165: }
166:
167: #[\Override]
168: protected function getResultType(): string
169: {
170: return ResultType::Task->value;
171: }
172: }
173: