[Server] Defer element loading to first registry read#389
Open
soyuka wants to merge 1 commit into
Open
Conversation
1424746 to
91b4290
Compare
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.
91b4290 to
d393af6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builder::build()runs all loaders eagerly (ChainLoader::load($registry)) and then snapshots server capabilities from that registry (tools: $registry->hasTools(), etc.). Both happen once, when the server is built.Under a persistent runtime (e.g. FrankenPHP worker mode) the server is built a single time and reused across requests. If a loader's data source is not yet ready at that moment — a cold cache, an application metadata layer that gets warmed later — the registry captures an empty state and never recovers for the lifetime of the process.
tools/list(which reads the registry) returns[], whiletools/callcan still work if the consumer resolves handlers independently of the registry. This was reported downstream in api-platform/core#8370 (FrankenPHP worker mode).Change
Mcp\Capability\LazyRegistry, a registry decorator that runs the loaders on the first read instead of at build time. Loading moves to request time, when the application is fully initialized; it runs once per process, and registrations made before the first read are preserved (loader writes are additive/idempotent by name).Builder::build(), wrap the registry inLazyRegistryinstead of calling$loader->load()eagerly.$this->tools,$this->explicitTools, discovery/custom loaders, …) rather than from the now-lazy registry, so theinitializehandshake does not force an eager load. Custom loaders and discovery are opaque about which element kinds they yield, so their presence advertises tools/resources/prompts; over-advertising is harmless (a client lists and gets an empty result).setRegistry()is treated as an opaque source too, so a pre-populated custom registry still advertises its capabilities (it previously advertisedfalseafter the switch away from inspecting the registry, which would make a spec-compliant client skiptools/listentirely).This also removes the need for the userland workarounds consumers currently apply (request-time list handlers, restoring-registry decorators) to keep discovery working under worker runtimes.
Tests
LazyRegistryTest: loader not run until first read, runs on first read and populates, runs exactly once across many reads, runtime registrations survive the deferred load, andtestLoaderRetriesAfterAFailedLoad(a throwing first load is retried on the next read).BuilderTest::testBuildAdvertisesToolsForPreloadedCustomRegistry: a registry pre-populated viasetRegistry()advertisestoolsin theinitializehandshake.BuilderTestand the full suite pass. PHP-CS-Fixer and PHPStan clean.Notes
Behavioral change worth flagging for review: capabilities are now derived from configured sources, so a server with a discovery path or custom loader that ultimately yields nothing will advertise
tools/resources/promptswhere it previously advertisedfalse. This is intentional (the whole point is not to inspect the registry at build), and harmless per MCP semantics, but it is a visible difference in theinitializeresponse.