Skip to content

Commit efe0366

Browse files
committed
[Server][Capability] Let setLazyLoading() defer loading into any supplied registry
Refs #495
1 parent 66ca225 commit efe0366

15 files changed

Lines changed: 721 additions & 79 deletions

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ All notable changes to `mcp/sdk` will be documented in this file.
2727
* Fix the result of a retried `resources/read` leaving out the required `ttlMs` and `cacheScope`, which the TypeScript SDK's client rejects, or keeping the hints its handler set: it now always carries `ttlMs: 0` and `cacheScope: private`.
2828
* Fix tool name validation not following the spec's rule (1 to 128 characters of `A-Z`, `a-z`, `0-9`, `_`, `-` and `.`): names of 65 to 128 characters no longer log a warning, while names with a `/` or a trailing newline now do.
2929
* [BC Break] `ReferenceHandler` resolves a string handler that is not an invokable class from the container, so a loader can register a service id as handler. The container goes first, so a function handler no longer runs if the container has an entry of the same name.
30+
* [BC Break] `Builder::setLazyLoading(true)` now also defers loading into a registry passed to `setRegistry()`, which any `RegistryInterface` supports. A registry you read directly stays empty until the server first reads it; without the call it is still loaded at build.
31+
* [BC Break] Detect the server capabilities when a request asks for them, so `initialize`, `server/discover` and `subscriptions/listen` run a pending load. The server advertises what the registry holds, and no longer every kind for a custom loader or discovery that was not loaded yet. While a load is still running, it advertises all kinds. `Configuration::$capabilities` is replaced by `Configuration::capabilities()`, and the constructor also takes a closure.
32+
* Add `DeferredRegistryInterface`, so the builder loads a custom registry without announcing the load as list changes, and no longer depends on the concrete `Registry`.
3033
* Split `RegistryInterface` by role into `ElementCatalogInterface`, `ElementResolverInterface` and `ElementWriterInterface`, which it extends. Request handlers depend on the role they use, so a handler can run against a catalog or resolver alone.
3134
* Add `Psr6SessionStore` and `Psr6NotificationBus`, which work like `Psr16SessionStore` and `Psr16NotificationBus` over a PSR-6 cache pool. `Builder::setDiscovery()` also accepts a PSR-6 pool as `$cache`.
3235
* Fix a nullable tool parameter with a default (e.g. `?string $category = 'books'`) receiving the default when the client explicitly sends `null`: `ReferenceHandler` now passes the `null` through.

‎docs/run/server-builder.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,8 +487,8 @@ $server = Server::builder()
487487
| `setSchemaGenerator()` | schemaGenerator | Replace the generator that builds input schemas from PHP types, see [Schema generation](../servers/schemas.md) |
488488
| `setSchemaValidator()` | schemaValidator | Replace the validator for tool arguments and structured output, see [Schema generation](../servers/schemas.md) |
489489
| `setReferenceHandler()` | referenceHandler | Replace the class that calls the handlers of tools, resources and prompts |
490-
| `setRegistry()` | registry | Use your own registry. It is loaded when the server is built |
491-
| `setLazyLoading()` | lazyLoading? | Load elements on the first registry read (default) or, with `false`, when the server is built |
490+
| `setRegistry()` | registry | Use your own registry. It is loaded when the server is built, unless you call `setLazyLoading()` |
491+
| `setLazyLoading()` | lazyLoading? | Load elements on the first registry read, which is at the latest when a client asks for the capabilities (default without `setRegistry()`), or, with `false`, when the server is built. Your own registry stays empty until the server first reads it |
492492
| `addLoader()` | loader | Add a custom loader that registers elements |
493493
| `addLoaders()` | loaders | Add several custom loaders |
494494
| `addRequestHandler()` | handler | Prepend a single custom request handler |
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Capability;
13+
14+
use Mcp\Capability\Registry\Loader\LoaderInterface;
15+
16+
/**
17+
* A registry that takes part in its own load lifecycle.
18+
*
19+
* @author Christopher Hertel <mail@christopher-hertel.de>
20+
*/
21+
interface DeferredRegistryInterface
22+
{
23+
/**
24+
* Runs a pending load, if there is one. A failed load stays pending.
25+
*/
26+
public function load(): void;
27+
28+
public function isLoaded(): bool;
29+
30+
/**
31+
* Runs $loader against this registry without announcing its writes as list changes, since they
32+
* describe the registry filling up rather than changing.
33+
*/
34+
public function loadFrom(LoaderInterface $loader): void;
35+
}

‎src/Capability/Registry.php‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
*
4141
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
4242
*/
43-
final class Registry implements RegistryInterface
43+
final class Registry implements RegistryInterface, DeferredRegistryInterface
4444
{
4545
/**
4646
* @var array<string, ToolReference>
@@ -94,6 +94,11 @@ public function load(): void
9494
$this->loaded = true;
9595
}
9696

97+
public function isLoaded(): bool
98+
{
99+
return $this->loaded || null === $this->loader;
100+
}
101+
97102
/**
98103
* Runs $loader with the change events its registrations would dispatch suppressed, since they
99104
* describe the registry filling up rather than changing.
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Capability\Registry;
13+
14+
use Mcp\Capability\DeferredRegistryInterface;
15+
use Mcp\Capability\Registry\Loader\LoaderInterface;
16+
use Mcp\Capability\RegistryInterface;
17+
use Mcp\Schema\Page;
18+
use Mcp\Schema\Prompt;
19+
use Mcp\Schema\ResourceDefinition;
20+
use Mcp\Schema\ResourceTemplate;
21+
use Mcp\Schema\Tool;
22+
23+
/**
24+
* Defers a loader over any registry to the first read, leaving the registry itself unaware of it.
25+
*
26+
* @internal
27+
*
28+
* @author Christopher Hertel <mail@christopher-hertel.de>
29+
*/
30+
final class LazyRegistry implements RegistryInterface, DeferredRegistryInterface
31+
{
32+
private bool $loaded = false;
33+
private bool $loading = false;
34+
35+
public function __construct(
36+
private readonly RegistryInterface $registry,
37+
private readonly LoaderInterface $loader,
38+
) {
39+
}
40+
41+
public function load(): void
42+
{
43+
if ($this->loading) {
44+
return;
45+
}
46+
47+
if (!$this->loaded) {
48+
$this->loading = true;
49+
try {
50+
$this->loadFrom($this->loader);
51+
$this->loaded = true;
52+
} finally {
53+
$this->loading = false;
54+
}
55+
}
56+
57+
if ($this->registry instanceof DeferredRegistryInterface) {
58+
$this->registry->load();
59+
}
60+
}
61+
62+
public function isLoaded(): bool
63+
{
64+
return $this->loaded && (!$this->registry instanceof DeferredRegistryInterface || $this->registry->isLoaded());
65+
}
66+
67+
public function loadFrom(LoaderInterface $loader): void
68+
{
69+
if ($this->registry instanceof DeferredRegistryInterface) {
70+
$this->registry->loadFrom($loader);
71+
72+
return;
73+
}
74+
75+
$loader->load($this->registry);
76+
}
77+
78+
public function publishesListChanges(): bool
79+
{
80+
return $this->registry->publishesListChanges();
81+
}
82+
83+
public function registerTool(Tool $tool, callable|array|string $handler): ToolReference
84+
{
85+
return $this->registry->registerTool($tool, $handler);
86+
}
87+
88+
public function registerResource(ResourceDefinition $resource, callable|array|string $handler): ResourceReference
89+
{
90+
return $this->registry->registerResource($resource, $handler);
91+
}
92+
93+
public function registerResourceTemplate(ResourceTemplate $template, callable|array|string $handler, array $completionProviders = []): ResourceTemplateReference
94+
{
95+
return $this->registry->registerResourceTemplate($template, $handler, $completionProviders);
96+
}
97+
98+
public function registerPrompt(Prompt $prompt, callable|array|string $handler, array $completionProviders = []): PromptReference
99+
{
100+
return $this->registry->registerPrompt($prompt, $handler, $completionProviders);
101+
}
102+
103+
public function unregisterTool(string $name): void
104+
{
105+
$this->registry->unregisterTool($name);
106+
}
107+
108+
public function unregisterResource(string $uri): void
109+
{
110+
$this->registry->unregisterResource($uri);
111+
}
112+
113+
public function unregisterResourceTemplate(string $uriTemplate): void
114+
{
115+
$this->registry->unregisterResourceTemplate($uriTemplate);
116+
}
117+
118+
public function unregisterPrompt(string $name): void
119+
{
120+
$this->registry->unregisterPrompt($name);
121+
}
122+
123+
public function hasTool(string $name): bool
124+
{
125+
$this->load();
126+
127+
return $this->registry->hasTool($name);
128+
}
129+
130+
public function hasResource(string $uri): bool
131+
{
132+
$this->load();
133+
134+
return $this->registry->hasResource($uri);
135+
}
136+
137+
public function hasResourceTemplate(string $uriTemplate): bool
138+
{
139+
$this->load();
140+
141+
return $this->registry->hasResourceTemplate($uriTemplate);
142+
}
143+
144+
public function hasPrompt(string $name): bool
145+
{
146+
$this->load();
147+
148+
return $this->registry->hasPrompt($name);
149+
}
150+
151+
public function hasTools(): bool
152+
{
153+
$this->load();
154+
155+
return $this->registry->hasTools();
156+
}
157+
158+
public function hasResources(): bool
159+
{
160+
$this->load();
161+
162+
return $this->registry->hasResources();
163+
}
164+
165+
public function hasResourceTemplates(): bool
166+
{
167+
$this->load();
168+
169+
return $this->registry->hasResourceTemplates();
170+
}
171+
172+
public function hasPrompts(): bool
173+
{
174+
$this->load();
175+
176+
return $this->registry->hasPrompts();
177+
}
178+
179+
public function getTools(?int $limit = null, ?string $cursor = null): Page
180+
{
181+
$this->load();
182+
183+
return $this->registry->getTools($limit, $cursor);
184+
}
185+
186+
public function getResources(?int $limit = null, ?string $cursor = null): Page
187+
{
188+
$this->load();
189+
190+
return $this->registry->getResources($limit, $cursor);
191+
}
192+
193+
public function getResourceTemplates(?int $limit = null, ?string $cursor = null): Page
194+
{
195+
$this->load();
196+
197+
return $this->registry->getResourceTemplates($limit, $cursor);
198+
}
199+
200+
public function getPrompts(?int $limit = null, ?string $cursor = null): Page
201+
{
202+
$this->load();
203+
204+
return $this->registry->getPrompts($limit, $cursor);
205+
}
206+
207+
public function getTool(string $name): ToolReference
208+
{
209+
$this->load();
210+
211+
return $this->registry->getTool($name);
212+
}
213+
214+
public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference
215+
{
216+
$this->load();
217+
218+
return $this->registry->getResource($uri, $includeTemplates);
219+
}
220+
221+
public function getResourceTemplate(string $uriTemplate): ResourceTemplateReference
222+
{
223+
$this->load();
224+
225+
return $this->registry->getResourceTemplate($uriTemplate);
226+
}
227+
228+
public function getPrompt(string $name): PromptReference
229+
{
230+
$this->load();
231+
232+
return $this->registry->getPrompt($name);
233+
}
234+
}

0 commit comments

Comments
 (0)