Skip to content

Commit 1424746

Browse files
committed
[Server] Defer element loading to first registry read
Builder::build() runs all loaders eagerly and snapshots capabilities from the resulting registry. Both are computed once, when the server is built. Under a persistent runtime (e.g. FrankenPHP worker mode) the server is built a single time, so a loader whose data source is not yet ready at that moment (cold cache, un-warmed metadata) leaves the registry empty for the whole process — tools/list stays empty while tools/call still works. Wrap the registry in a LazyRegistry that runs the loaders on the first read, moving loading to request time when the application is initialized. The load is retried on the next read if it throws, so a transient failure at the first read does not freeze an empty registry for the whole process. Advertise capabilities from the configured element sources instead of the loaded registry, so the initialize handshake does not force an eager load. A registry supplied via setRegistry() counts as an opaque source too, so a pre-populated custom registry still advertises its capabilities.
1 parent 30e520a commit 1424746

4 files changed

Lines changed: 380 additions & 6 deletions

File tree

src/Capability/LazyRegistry.php

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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+
use Mcp\Capability\Registry\PromptReference;
16+
use Mcp\Capability\Registry\ResourceReference;
17+
use Mcp\Capability\Registry\ResourceTemplateReference;
18+
use Mcp\Capability\Registry\ToolReference;
19+
use Mcp\Schema\Page;
20+
use Mcp\Schema\Prompt;
21+
use Mcp\Schema\ResourceDefinition;
22+
use Mcp\Schema\ResourceTemplate;
23+
use Mcp\Schema\Tool;
24+
25+
/**
26+
* Decorates a registry so its loader runs on first read instead of eagerly at build time.
27+
*
28+
* Running loaders in {@see \Mcp\Server\Builder::build()} freezes the registry to whatever the
29+
* loaders can see at build time. Under a persistent runtime (e.g. FrankenPHP worker mode) the
30+
* server is built once, so if a loader's data source is not ready at that moment — a cold cache,
31+
* a not-yet-warmed metadata layer — the registry captures an empty state and never recovers.
32+
*
33+
* Deferring the load to the first read moves it to request time, when the application is fully
34+
* initialized. The load runs once per process; subsequent reads hit the populated inner registry.
35+
* Registrations are delegated straight through, so runtime registrations made before the first
36+
* read are preserved (the loader's registrations are additive/idempotent by name).
37+
*
38+
* @author Antoine Bluchet <soyuka@gmail.com>
39+
*/
40+
final class LazyRegistry implements RegistryInterface
41+
{
42+
private bool $loaded = false;
43+
44+
public function __construct(
45+
private readonly RegistryInterface $registry,
46+
private readonly LoaderInterface $loader,
47+
) {
48+
}
49+
50+
public function registerTool(Tool $tool, callable|array|string $handler): ToolReference
51+
{
52+
return $this->registry->registerTool($tool, $handler);
53+
}
54+
55+
public function registerResource(ResourceDefinition $resource, callable|array|string $handler): ResourceReference
56+
{
57+
return $this->registry->registerResource($resource, $handler);
58+
}
59+
60+
public function registerResourceTemplate(ResourceTemplate $template, callable|array|string $handler, array $completionProviders = []): ResourceTemplateReference
61+
{
62+
return $this->registry->registerResourceTemplate($template, $handler, $completionProviders);
63+
}
64+
65+
public function registerPrompt(Prompt $prompt, callable|array|string $handler, array $completionProviders = []): PromptReference
66+
{
67+
return $this->registry->registerPrompt($prompt, $handler, $completionProviders);
68+
}
69+
70+
public function unregisterTool(string $name): void
71+
{
72+
$this->registry->unregisterTool($name);
73+
}
74+
75+
public function unregisterResource(string $uri): void
76+
{
77+
$this->registry->unregisterResource($uri);
78+
}
79+
80+
public function unregisterResourceTemplate(string $uriTemplate): void
81+
{
82+
$this->registry->unregisterResourceTemplate($uriTemplate);
83+
}
84+
85+
public function unregisterPrompt(string $name): void
86+
{
87+
$this->registry->unregisterPrompt($name);
88+
}
89+
90+
public function hasTool(string $name): bool
91+
{
92+
$this->load();
93+
94+
return $this->registry->hasTool($name);
95+
}
96+
97+
public function hasResource(string $uri): bool
98+
{
99+
$this->load();
100+
101+
return $this->registry->hasResource($uri);
102+
}
103+
104+
public function hasResourceTemplate(string $uriTemplate): bool
105+
{
106+
$this->load();
107+
108+
return $this->registry->hasResourceTemplate($uriTemplate);
109+
}
110+
111+
public function hasPrompt(string $name): bool
112+
{
113+
$this->load();
114+
115+
return $this->registry->hasPrompt($name);
116+
}
117+
118+
public function hasTools(): bool
119+
{
120+
$this->load();
121+
122+
return $this->registry->hasTools();
123+
}
124+
125+
public function getTools(?int $limit = null, ?string $cursor = null): Page
126+
{
127+
$this->load();
128+
129+
return $this->registry->getTools($limit, $cursor);
130+
}
131+
132+
public function getTool(string $name): ToolReference
133+
{
134+
$this->load();
135+
136+
return $this->registry->getTool($name);
137+
}
138+
139+
public function hasResources(): bool
140+
{
141+
$this->load();
142+
143+
return $this->registry->hasResources();
144+
}
145+
146+
public function getResources(?int $limit = null, ?string $cursor = null): Page
147+
{
148+
$this->load();
149+
150+
return $this->registry->getResources($limit, $cursor);
151+
}
152+
153+
public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference
154+
{
155+
$this->load();
156+
157+
return $this->registry->getResource($uri, $includeTemplates);
158+
}
159+
160+
public function hasResourceTemplates(): bool
161+
{
162+
$this->load();
163+
164+
return $this->registry->hasResourceTemplates();
165+
}
166+
167+
public function getResourceTemplates(?int $limit = null, ?string $cursor = null): Page
168+
{
169+
$this->load();
170+
171+
return $this->registry->getResourceTemplates($limit, $cursor);
172+
}
173+
174+
public function getResourceTemplate(string $uriTemplate): ResourceTemplateReference
175+
{
176+
$this->load();
177+
178+
return $this->registry->getResourceTemplate($uriTemplate);
179+
}
180+
181+
public function hasPrompts(): bool
182+
{
183+
$this->load();
184+
185+
return $this->registry->hasPrompts();
186+
}
187+
188+
public function getPrompts(?int $limit = null, ?string $cursor = null): Page
189+
{
190+
$this->load();
191+
192+
return $this->registry->getPrompts($limit, $cursor);
193+
}
194+
195+
public function getPrompt(string $name): PromptReference
196+
{
197+
$this->load();
198+
199+
return $this->registry->getPrompt($name);
200+
}
201+
202+
private function load(): void
203+
{
204+
if ($this->loaded) {
205+
return;
206+
}
207+
208+
$this->loader->load($this->registry);
209+
$this->loaded = true;
210+
}
211+
}

src/Server/Builder.php

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Mcp\Capability\Discovery\Discoverer;
1717
use Mcp\Capability\Discovery\DiscovererInterface;
1818
use Mcp\Capability\Discovery\SchemaGeneratorInterface;
19+
use Mcp\Capability\LazyRegistry;
1920
use Mcp\Capability\Registry;
2021
use Mcp\Capability\Registry\Container;
2122
use Mcp\Capability\Registry\ElementReference;
@@ -219,6 +220,8 @@ final class Builder
219220
*/
220221
private array $loaders = [];
221222

223+
private bool $hasCustomRegistry = false;
224+
222225
/**
223226
* Sets the server's identity. Required.
224227
*
@@ -344,6 +347,7 @@ public function addNotificationHandlers(iterable $handlers): self
344347
public function setRegistry(RegistryInterface $registry): self
345348
{
346349
$this->registry = $registry;
350+
$this->hasCustomRegistry = true;
347351

348352
return $this;
349353
}
@@ -674,18 +678,29 @@ public function build(): Server
674678
}
675679
}
676680

677-
$loader = new ChainLoader($loaders);
678-
$loader->load($registry);
681+
// Load lazily, on the first registry read, instead of eagerly here. Eager loading freezes
682+
// the registry to what the loaders see at build time; under a persistent runtime (e.g.
683+
// FrankenPHP worker mode) the server is built once, so a loader whose data source is not
684+
// yet ready at build (cold cache, un-warmed metadata) would leave the registry empty for
685+
// the whole process. Deferring to request time avoids that. @see LazyRegistry
686+
$registry = new LazyRegistry($registry, new ChainLoader($loaders));
679687

680688
$messageFactory = MessageFactory::make();
681689

690+
// Capabilities are advertised from the configured element sources rather than from the
691+
// (now lazily loaded) registry, so the initialize handshake does not force an eager load.
692+
// Custom loaders, discovery, and a caller-supplied registry are opaque about which element
693+
// kinds they yield, so their presence advertises tools, resources and prompts;
694+
// over-advertising is harmless (a client simply lists and gets an empty result).
695+
$hasOpaqueSources = [] !== $this->loaders || null !== $this->discoveryBasePath || $this->hasCustomRegistry;
696+
682697
$capabilities = $this->serverCapabilities ?? new ServerCapabilities(
683-
tools: $registry->hasTools(),
698+
tools: [] !== $this->tools || [] !== $this->explicitTools || $hasOpaqueSources,
684699
toolsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
685-
resources: $registry->hasResources() || $registry->hasResourceTemplates(),
686-
resourcesSubscribe: $registry->hasResources() || $registry->hasResourceTemplates(),
700+
resources: [] !== $this->resources || [] !== $this->explicitResources || [] !== $this->resourceTemplates || [] !== $this->explicitResourceTemplates || $hasOpaqueSources,
701+
resourcesSubscribe: [] !== $this->resources || [] !== $this->explicitResources || [] !== $this->resourceTemplates || [] !== $this->explicitResourceTemplates || $hasOpaqueSources,
687702
resourcesListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
688-
prompts: $registry->hasPrompts(),
703+
prompts: [] !== $this->prompts || [] !== $this->explicitPrompts || $hasOpaqueSources,
689704
promptsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
690705
logging: true,
691706
completions: true,

0 commit comments

Comments
 (0)