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\Resource;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Exception\InvalidParamsException;
18: use Nexus\Mcp\Core\SafeDisplay;
19: use Nexus\Mcp\Core\Schema\Cursor;
20: use Nexus\Mcp\Core\Schema\Enum\CacheScope;
21: use Nexus\Mcp\Core\Schema\Resource\Resource;
22: use Nexus\Mcp\Core\Schema\Result\InputRequiredResult;
23: use Nexus\Mcp\Core\Schema\Result\ListResourcesResult;
24: use Nexus\Mcp\Core\Schema\Result\ReadResourceResult;
25: use Nexus\Mcp\Core\Validation\IconSrcValidator;
26: use Nexus\Mcp\Core\Validation\IdentifierNameValidator;
27: use Nexus\Mcp\Server\CursorPaginator;
28: use Nexus\Mcp\Server\Exception\ResourceNotRegisteredException;
29: use Nexus\Mcp\Server\ServerContext;
30:
31: /**
32: * In-memory implementation of `MutableResourceStoreInterface`.
33: */
34: final class ResourceStore implements MutableResourceStoreInterface
35: {
36: private readonly CursorPaginator $paginator;
37:
38: /**
39: * @var list<\Closure(): void>
40: */
41: private array $listChangedListeners = [];
42:
43: /**
44: * @param array<non-empty-string, ResourceEntry> $entries
45: */
46: public function __construct(
47: private array $entries = [],
48: int $pageSize = CursorPaginator::DEFAULT_PAGE_SIZE,
49: private readonly int $ttlMs = 0,
50: private readonly CacheScope $cacheScope = CacheScope::Private,
51: ) {
52: Assert::that($entries)
53: ->keys()
54: ->isNonEmptyString('Resource store entry key must be a non-empty string.')
55: ;
56:
57: foreach ($entries as $key => $entry) {
58: IdentifierNameValidator::validate($entry->resource->name, 'resource "name"');
59: IconSrcValidator::validate($entry->resource->icons, 'resource');
60:
61: Assert::that($entry->resource->uri)
62: ->isIdentical($key, 'Resource store entry key "{other}" must match its resource URI "{value}".')
63: ;
64: }
65:
66: Assert::that($pageSize)->isPositiveInt('Resource store page size must be a positive integer, {value} given.');
67: Assert::that($ttlMs)->isNaturalInt('Resource store TTL must be a non-negative integer, {value} given.');
68:
69: $this->paginator = new CursorPaginator($pageSize);
70: }
71:
72: #[\Override]
73: public function onListChanged(\Closure $listener): void
74: {
75: $this->listChangedListeners[] = $listener;
76: }
77:
78: #[\Override]
79: public function addResource(Resource $resource, ResourceReaderInterface $reader): void
80: {
81: IdentifierNameValidator::validate($resource->name, 'resource "name"');
82: IconSrcValidator::validate($resource->icons, 'resource');
83:
84: $this->entries[$resource->uri] = new ResourceEntry($resource, $reader);
85:
86: $this->announceListChange();
87: }
88:
89: #[\Override]
90: public function removeResource(string $uri): bool
91: {
92: if (! \array_key_exists($uri, $this->entries)) {
93: return false;
94: }
95:
96: unset($this->entries[$uri]);
97:
98: $this->announceListChange();
99:
100: return true;
101: }
102:
103: #[\Override]
104: public function list(?Cursor $cursor): ListResourcesResult
105: {
106: $page = $this->paginator->paginate($this->entries, $cursor);
107:
108: return new ListResourcesResult(
109: resources: array_map(static fn(ResourceEntry $entry): Resource => $entry->resource, $page->entries),
110: ttlMs: $this->ttlMs,
111: cacheScope: $this->cacheScope,
112: nextCursor: $page->nextCursor,
113: );
114: }
115:
116: #[\Override]
117: public function read(string $uri, ServerContext $context): InputRequiredResult|ReadResourceResult
118: {
119: $entry = $this->entries[$uri] ?? throw new ResourceNotRegisteredException($uri, $context->requestId);
120:
121: try {
122: return $entry->reader->read($uri, $context);
123: } catch (InvalidParamsException $e) {
124: throw new InvalidParamsException(
125: $context->requestId,
126: SafeDisplay::sanitiseCause(\sprintf('Invalid arguments for resource "%s": %s', $uri, $e->getMessage())),
127: );
128: }
129: }
130:
131: private function announceListChange(): void
132: {
133: foreach ($this->listChangedListeners as $listener) {
134: $listener();
135: }
136: }
137: }
138: