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\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\Prompt\Prompt;
22: use Nexus\Mcp\Core\Schema\Result\GetPromptResult;
23: use Nexus\Mcp\Core\Schema\Result\InputRequiredResult;
24: use Nexus\Mcp\Core\Schema\Result\ListPromptsResult;
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\PromptNotFoundException;
29: use Nexus\Mcp\Server\ServerContext;
30:
31: /**
32: * In-memory implementation of `MutablePromptStoreInterface`.
33: */
34: final class PromptStore implements MutablePromptStoreInterface
35: {
36: private readonly CursorPaginator $paginator;
37:
38: /**
39: * @var list<\Closure(): void>
40: */
41: private array $listChangedListeners = [];
42:
43: /**
44: * @param array<int|non-empty-string, PromptEntry> $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: foreach ($entries as $key => $entry) {
53: IdentifierNameValidator::validate($entry->prompt->name, 'prompt "name"');
54: IconSrcValidator::validate($entry->prompt->icons, 'prompt');
55:
56: Assert::that($entry->prompt->name)->isIdentical(
57: (string) $key,
58: 'Prompt store entry key "{other}" must match its prompt name "{value}".',
59: );
60: }
61:
62: Assert::that($pageSize)
63: ->isPositiveInt('Prompt store page size must be a positive integer, {value} given.')
64: ;
65: Assert::that($ttlMs)
66: ->isNaturalInt('Prompt store TTL must be a non-negative integer, {value} given.')
67: ;
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 addPrompt(Prompt $prompt, PromptRendererInterface $renderer): void
80: {
81: IdentifierNameValidator::validate($prompt->name, 'prompt "name"');
82: IconSrcValidator::validate($prompt->icons, 'prompt');
83:
84: $this->entries[$prompt->name] = new PromptEntry($prompt, $renderer);
85:
86: $this->announceListChange();
87: }
88:
89: #[\Override]
90: public function removePrompt(string $name): bool
91: {
92: if (! \array_key_exists($name, $this->entries)) {
93: return false;
94: }
95:
96: unset($this->entries[$name]);
97:
98: $this->announceListChange();
99:
100: return true;
101: }
102:
103: #[\Override]
104: public function list(?Cursor $cursor): ListPromptsResult
105: {
106: $page = $this->paginator->paginate($this->entries, $cursor);
107:
108: return new ListPromptsResult(
109: prompts: array_map(static fn(PromptEntry $entry): Prompt => $entry->prompt, $page->entries),
110: ttlMs: $this->ttlMs,
111: cacheScope: $this->cacheScope,
112: nextCursor: $page->nextCursor,
113: );
114: }
115:
116: #[\Override]
117: public function get(string $name, ?array $arguments, ServerContext $context): GetPromptResult|InputRequiredResult
118: {
119: if (! \array_key_exists($name, $this->entries)) {
120: throw new PromptNotFoundException($name, $context->requestId);
121: }
122:
123: try {
124: return $this->entries[$name]->renderer->render($arguments, $context);
125: } catch (InvalidParamsException $e) {
126: throw new InvalidParamsException(
127: $context->requestId,
128: SafeDisplay::sanitiseCause(\sprintf('Invalid arguments for prompt "%s": %s', $name, $e->getMessage())),
129: );
130: }
131: }
132:
133: private function announceListChange(): void
134: {
135: foreach ($this->listChangedListeners as $listener) {
136: $listener();
137: }
138: }
139: }
140: