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\Skills\Schema;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\Arrayable;
18:
19: /**
20: * One file of a skill's manifest.
21: *
22: * @implements Arrayable<array{
23: * uri: non-empty-string,
24: * digest: non-empty-string,
25: * size: int<0, max>,
26: * }>
27: *
28: * @see https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2640-skills-extension.md
29: */
30: final readonly class SkillResource implements Arrayable
31: {
32: public const string DIGEST_PREFIX = 'sha256:';
33: public const string DIGEST_PATTERN = '/\Asha256:[0-9a-f]{64}\z/';
34:
35: /**
36: * @param non-empty-string $uri
37: * @param non-empty-string $digest `sha256:` and the 64 lowercase hexadecimal characters of the file's SHA-256
38: * @param int<0, max> $size Byte length of the file's raw content
39: */
40: public function __construct(
41: public string $uri,
42: public string $digest,
43: public int $size,
44: ) {
45: Assert::that($uri)->isNonEmptyString('skill resource "uri" must be a non-empty string.');
46: Assert::that($digest)->matchesRegularExpression(
47: self::DIGEST_PATTERN,
48: 'skill resource "digest" must be "sha256:" followed by 64 lowercase hexadecimal characters, {value} given.',
49: );
50: Assert::that($size)->isNaturalInt('skill resource "size" must be a non-negative integer, {value} given.');
51: }
52:
53: /**
54: * @internal
55: *
56: * @return non-empty-string
57: */
58: public static function computeDigest(string $bytes): string
59: {
60: return self::DIGEST_PREFIX.hash('sha256', $bytes);
61: }
62:
63: #[\Override]
64: public static function fromArray(array $data): static
65: {
66: Assert::that($data)->hasOffset('uri', 'skill resource is missing the required "uri" key.');
67: $uri = $data['uri'];
68: Assert::that($uri)->isNonEmptyString('skill resource "uri" must be a non-empty string, {type} given.');
69:
70: Assert::that($data)->hasOffset('digest', 'skill resource is missing the required "digest" key.');
71: $digest = $data['digest'];
72: Assert::that($digest)->isNonEmptyString('skill resource "digest" must be a non-empty string, {type} given.');
73:
74: Assert::that($data)->hasOffset('size', 'skill resource is missing the required "size" key.');
75: $size = $data['size'];
76: Assert::that($size)->isNaturalInt('skill resource "size" must be a non-negative integer, {type} given.');
77:
78: return new self(uri: $uri, digest: $digest, size: $size);
79: }
80:
81: #[\Override]
82: public function toArray(): array
83: {
84: return [
85: 'uri' => $this->uri,
86: 'digest' => $this->digest,
87: 'size' => $this->size,
88: ];
89: }
90:
91: #[\Override]
92: public function jsonSerialize(): array
93: {
94: return $this->toArray();
95: }
96: }
97: