|
26 | 26 | use Mcp\Schema\Tool; |
27 | 27 | use Mcp\Schema\ToolAnnotations; |
28 | 28 | use Mcp\Server\Capability\Completion\ProviderInterface; |
| 29 | +use Mcp\Server\Capability\DeferredRegistryInterface; |
29 | 30 | use Mcp\Server\Capability\Discovery\CachedDiscoverer; |
30 | 31 | use Mcp\Server\Capability\Discovery\Discoverer; |
31 | 32 | use Mcp\Server\Capability\Discovery\DiscovererInterface; |
|
34 | 35 | use Mcp\Server\Capability\Registry; |
35 | 36 | use Mcp\Server\Capability\Registry\Container; |
36 | 37 | use Mcp\Server\Capability\Registry\ElementReference; |
| 38 | +use Mcp\Server\Capability\Registry\LazyRegistry; |
37 | 39 | use Mcp\Server\Capability\Registry\Loader\ChainLoader; |
38 | 40 | use Mcp\Server\Capability\Registry\Loader\DiscoveryLoader; |
39 | 41 | use Mcp\Server\Capability\Registry\Loader\ExplicitElementLoader; |
@@ -276,7 +278,7 @@ final class Builder |
276 | 278 |
|
277 | 279 | private bool $hasCustomRegistry = false; |
278 | 280 |
|
279 | | - private bool $lazyLoading = true; |
| 281 | + private ?bool $lazyLoading = null; |
280 | 282 |
|
281 | 283 | private bool $headerValidation = true; |
282 | 284 |
|
@@ -596,8 +598,8 @@ public function setRegistry(RegistryInterface $registry): self |
596 | 598 | * |
597 | 599 | * Lazy (the default) defers loading to the first registry read so a persistent runtime does not |
598 | 600 | * freeze the registry to a source not yet ready at build time. Disable to load eagerly at build. |
599 | | - * A registry supplied via setRegistry() is always loaded eagerly; its own constructor loader, |
600 | | - * if it has one, still runs on the first read. |
| 601 | + * A registry supplied via setRegistry() is loaded at build unless lazy loading is asked for explicitly. |
| 602 | + * Then it stays unloaded until the server first reads it, also for code holding the same instance. |
601 | 603 | */ |
602 | 604 | public function setLazyLoading(bool $lazyLoading = true): self |
603 | 605 | { |
@@ -1079,7 +1081,8 @@ public function buildStateless(array $supportedVersions = [ProtocolVersion::V202 |
1079 | 1081 | $discovery = $parts['configuration']; |
1080 | 1082 | // Without a bus, `subscriptions/listen` delivers nothing; `resources/subscribe` only exists on the handshake. |
1081 | 1083 | if (null === $this->notificationBus && null === $this->serverCapabilities) { |
1082 | | - $discovery = new Configuration($discovery->serverInfo, $discovery->capabilities->withoutResourcesSubscribe(), $discovery->paginationLimit, $discovery->instructions, $discovery->protocolVersion); |
| 1084 | + $configuration = $discovery; |
| 1085 | + $discovery = new Configuration($discovery->serverInfo, static fn (): ServerCapabilities => $configuration->capabilities()->withoutResourcesSubscribe(), $discovery->paginationLimit, $discovery->instructions, $discovery->protocolVersion); |
1083 | 1086 | } |
1084 | 1087 |
|
1085 | 1088 | return new StatelessProtocol( |
@@ -1160,35 +1163,31 @@ private function resolve(): array |
1160 | 1163 |
|
1161 | 1164 | $chainLoader = new ChainLoader($loaders); |
1162 | 1165 |
|
1163 | | - if ($this->hasCustomRegistry) { |
1164 | | - // Builder can't inject the loader into an already-constructed instance, so load it eagerly. |
1165 | | - // Via loadFrom(), which suppresses the change events the load would otherwise dispatch. |
| 1166 | + if ($this->hasCustomRegistry && true === $this->lazyLoading) { |
| 1167 | + $registry = new LazyRegistry($this->registry, $chainLoader); |
| 1168 | + } elseif ($this->hasCustomRegistry) { |
1166 | 1169 | $registry = $this->registry; |
1167 | | - if ($registry instanceof Registry) { |
| 1170 | + if ($registry instanceof DeferredRegistryInterface) { |
1168 | 1171 | $registry->loadFrom($chainLoader); |
1169 | 1172 | } else { |
1170 | 1173 | $chainLoader->load($registry); |
1171 | 1174 | } |
1172 | | - $eagerlyLoaded = true; |
1173 | 1175 | } else { |
1174 | 1176 | $registry = new Registry($eventDispatcher, $logger, loader: $chainLoader); |
1175 | | - if (!$this->lazyLoading) { |
| 1177 | + if (false === $this->lazyLoading) { |
1176 | 1178 | $registry->load(); |
1177 | 1179 | } |
1178 | | - $eagerlyLoaded = !$this->lazyLoading; |
1179 | 1180 | } |
1180 | 1181 |
|
1181 | 1182 | $messageFactory = MessageFactory::make(additional: $this->extensionMessages); |
1182 | 1183 | // `server/discover` exists in the modern era only. |
1183 | 1184 | $modernMessageFactory = MessageFactory::make(additional: [...$this->extensionMessages, DiscoverRequest::class]); |
1184 | 1185 |
|
1185 | | - $capabilities = $this->serverCapabilities ?? $this->detectCapabilities($registry, $eagerlyLoaded); |
1186 | | - |
1187 | 1186 | // Extensions enabled via enableExtension() are folded into caller-supplied |
1188 | 1187 | // capabilities too, so setCapabilities() does not silently drop them. |
1189 | | - if (null !== $this->serverCapabilities && [] !== $this->extensions) { |
1190 | | - $capabilities = $capabilities->withExtensions($this->extensions); |
1191 | | - } |
| 1188 | + $capabilities = null === $this->serverCapabilities |
| 1189 | + ? (new RegistryCapabilities($registry, null !== $this->notificationBus, $this->extensions))->detect(...) |
| 1190 | + : ([] !== $this->extensions ? $this->serverCapabilities->withExtensions($this->extensions) : $this->serverCapabilities); |
1192 | 1191 |
|
1193 | 1192 | if (null !== $this->protocolVersion && $this->protocolVersion->isModern()) { |
1194 | 1193 | $logger->warning('Configured protocol version cannot be reached through the "initialize" handshake, negotiating the handshake revisions instead.', [ |
@@ -1234,50 +1233,6 @@ private function resolve(): array |
1234 | 1233 | ]; |
1235 | 1234 | } |
1236 | 1235 |
|
1237 | | - /** |
1238 | | - * When loaded, capabilities are read from the registry. When deferred, reading it would force |
1239 | | - * the load, so they are advertised from the configured sources instead — opaque sources (custom |
1240 | | - * loaders, discovery) advertise all kinds, and over-advertising is harmless per MCP semantics. |
1241 | | - */ |
1242 | | - private function detectCapabilities(RegistryInterface $registry, bool $eagerlyLoaded): ServerCapabilities |
1243 | | - { |
1244 | | - // Only the bus delivers list changes, and only if the registry publishes to it. |
1245 | | - $listChanged = null !== $this->notificationBus && $registry->publishesListChanges(); |
1246 | | - |
1247 | | - if ($eagerlyLoaded) { |
1248 | | - $hasResources = $registry->hasResources() || $registry->hasResourceTemplates(); |
1249 | | - |
1250 | | - return new ServerCapabilities( |
1251 | | - tools: $registry->hasTools(), |
1252 | | - toolsListChanged: $listChanged, |
1253 | | - resources: $hasResources, |
1254 | | - resourcesSubscribe: $hasResources, |
1255 | | - resourcesListChanged: $listChanged, |
1256 | | - prompts: $registry->hasPrompts(), |
1257 | | - promptsListChanged: $listChanged, |
1258 | | - logging: true, |
1259 | | - completions: true, |
1260 | | - extensions: $this->extensions ?: null, |
1261 | | - ); |
1262 | | - } |
1263 | | - |
1264 | | - $hasOpaqueSources = [] !== $this->loaders || null !== $this->discoveryBasePath; |
1265 | | - $hasResources = [] !== $this->resources || [] !== $this->explicitResources || [] !== $this->resourceTemplates || [] !== $this->explicitResourceTemplates || $hasOpaqueSources; |
1266 | | - |
1267 | | - return new ServerCapabilities( |
1268 | | - tools: [] !== $this->tools || [] !== $this->explicitTools || $hasOpaqueSources, |
1269 | | - toolsListChanged: $listChanged, |
1270 | | - resources: $hasResources, |
1271 | | - resourcesSubscribe: $hasResources, |
1272 | | - resourcesListChanged: $listChanged, |
1273 | | - prompts: [] !== $this->prompts || [] !== $this->explicitPrompts || $hasOpaqueSources, |
1274 | | - promptsListChanged: $listChanged, |
1275 | | - logging: true, |
1276 | | - completions: true, |
1277 | | - extensions: $this->extensions ?: null, |
1278 | | - ); |
1279 | | - } |
1280 | | - |
1281 | 1236 | private function createDiscoverer(LoggerInterface $logger): DiscovererInterface |
1282 | 1237 | { |
1283 | 1238 | $discoverer = new Discoverer($logger, null, $this->schemaGenerator); |
|
0 commit comments