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