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\Prompt;
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\Prompt\Prompt;
20: use Nexus\Mcp\Core\Schema\Result\GetPromptResult;
21: use Nexus\Mcp\Core\Schema\Result\InputRequiredResult;
22: use Nexus\Mcp\Core\Schema\Result\ListPromptsResult;
23: use Nexus\Mcp\Server\CursorPaginator;
24: use Nexus\Mcp\Server\Exception\PromptNotFoundException;
25: use Nexus\Mcp\Server\ServerContext;
26:
27: /**
28: * In-memory implementation of `MutablePromptStoreInterface`.
29: */
30: final class PromptStore implements MutablePromptStoreInterface
31: {
32: private readonly CursorPaginator $paginator;
33:
34: /**
35: * @var list<\Closure(): void>
36: */
37: private array $listChangedListeners = [];
38:
39: /**
40: * @param array<array-key, PromptEntry> $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: foreach ($entries as $key => $entry) {
49: // A decimal-int-string name arrives as an int key, so the comparison is on the stringified key.
50: Assert::that($entry->prompt->name)->isIdentical(
51: (string) $key,
52: 'Prompt store entry key "{other}" must match its prompt name "{value}".',
53: );
54: }
55:
56: Assert::that($pageSize)
57: ->isPositiveInt('Prompt store page size must be a positive integer, {value} given.')
58: ;
59: Assert::that($ttlMs)
60: ->isNaturalInt('Prompt store TTL must be a non-negative integer, {value} given.')
61: ;
62:
63: $this->paginator = new CursorPaginator($pageSize);
64: }
65:
66: #[\Override]
67: public function onListChanged(\Closure $listener): void
68: {
69: $this->listChangedListeners[] = $listener;
70: }
71:
72: #[\Override]
73: public function addPrompt(Prompt $prompt, PromptRendererInterface $renderer): void
74: {
75: $this->entries[$prompt->name] = new PromptEntry($prompt, $renderer);
76:
77: $this->announceListChange();
78: }
79:
80: #[\Override]
81: public function removePrompt(string $name): bool
82: {
83: if (! \array_key_exists($name, $this->entries)) {
84: return false;
85: }
86:
87: unset($this->entries[$name]);
88:
89: $this->announceListChange();
90:
91: return true;
92: }
93:
94: #[\Override]
95: public function list(?Cursor $cursor): ListPromptsResult
96: {
97: $page = $this->paginator->paginate($this->entries, $cursor);
98:
99: return new ListPromptsResult(
100: prompts: array_map(static fn(PromptEntry $entry): Prompt => $entry->prompt, $page->entries),
101: ttlMs: $this->ttlMs,
102: cacheScope: $this->cacheScope,
103: nextCursor: $page->nextCursor,
104: );
105: }
106:
107: #[\Override]
108: public function get(string $name, ?array $arguments, ServerContext $context): GetPromptResult|InputRequiredResult
109: {
110: if (! \array_key_exists($name, $this->entries)) {
111: throw new PromptNotFoundException($name, $context->requestId);
112: }
113:
114: return $this->entries[$name]->renderer->render($arguments, $context);
115: }
116:
117: private function announceListChange(): void
118: {
119: foreach ($this->listChangedListeners as $listener) {
120: $listener();
121: }
122: }
123: }
124: