diff --git a/src/Capability/Registry.php b/src/Capability/Registry.php index f90a4ca1..794193f8 100644 --- a/src/Capability/Registry.php +++ b/src/Capability/Registry.php @@ -108,7 +108,7 @@ public function registerTool(Tool $tool, callable|array|string $handler): ToolRe $reference = new ToolReference($tool, $handler); $this->tools[$tool->name] = $reference; - $this->eventDispatcher?->dispatch(new ToolListChangedEvent()); + $this->dispatch(new ToolListChangedEvent()); return $reference; } @@ -118,7 +118,7 @@ public function registerResource(ResourceDefinition $resource, callable|array|st $reference = new ResourceReference($resource, $handler); $this->resources[$resource->uri] = $reference; - $this->eventDispatcher?->dispatch(new ResourceListChangedEvent()); + $this->dispatch(new ResourceListChangedEvent()); return $reference; } @@ -131,7 +131,7 @@ public function registerResourceTemplate( $reference = new ResourceTemplateReference($template, $handler, $completionProviders); $this->resourceTemplates[$template->uriTemplate] = $reference; - $this->eventDispatcher?->dispatch(new ResourceTemplateListChangedEvent()); + $this->dispatch(new ResourceTemplateListChangedEvent()); return $reference; } @@ -144,7 +144,7 @@ public function registerPrompt( $reference = new PromptReference($prompt, $handler, $completionProviders); $this->prompts[$prompt->name] = $reference; - $this->eventDispatcher?->dispatch(new PromptListChangedEvent()); + $this->dispatch(new PromptListChangedEvent()); return $reference; } @@ -157,7 +157,7 @@ public function unregisterTool(string $name): void unset($this->tools[$name]); - $this->eventDispatcher?->dispatch(new ToolListChangedEvent()); + $this->dispatch(new ToolListChangedEvent()); } public function unregisterResource(string $uri): void @@ -168,7 +168,7 @@ public function unregisterResource(string $uri): void unset($this->resources[$uri]); - $this->eventDispatcher?->dispatch(new ResourceListChangedEvent()); + $this->dispatch(new ResourceListChangedEvent()); } public function unregisterResourceTemplate(string $uriTemplate): void @@ -179,7 +179,7 @@ public function unregisterResourceTemplate(string $uriTemplate): void unset($this->resourceTemplates[$uriTemplate]); - $this->eventDispatcher?->dispatch(new ResourceTemplateListChangedEvent()); + $this->dispatch(new ResourceTemplateListChangedEvent()); } public function unregisterPrompt(string $name): void @@ -190,7 +190,7 @@ public function unregisterPrompt(string $name): void unset($this->prompts[$name]); - $this->eventDispatcher?->dispatch(new PromptListChangedEvent()); + $this->dispatch(new PromptListChangedEvent()); } public function hasTool(string $name): bool @@ -390,6 +390,18 @@ public function getPrompt(string $name): PromptReference return $this->prompts[$name] ?? throw new PromptNotFoundException($name); } + /** + * Suppressed while the deferred loader runs, since it only sets up initial state. + */ + private function dispatch(object $event): void + { + if ($this->loading) { + return; + } + + $this->eventDispatcher?->dispatch($event); + } + /** * Calculate next cursor for pagination. * diff --git a/tests/Unit/Capability/RegistryTest.php b/tests/Unit/Capability/RegistryTest.php index 0b796db6..cb325565 100644 --- a/tests/Unit/Capability/RegistryTest.php +++ b/tests/Unit/Capability/RegistryTest.php @@ -19,6 +19,7 @@ use Mcp\Capability\Registry\ResourceTemplateReference; use Mcp\Capability\Registry\ToolReference; use Mcp\Capability\RegistryInterface; +use Mcp\Event\ToolListChangedEvent; use Mcp\Exception\PromptNotFoundException; use Mcp\Exception\ResourceNotFoundException; use Mcp\Exception\ToolNotFoundException; @@ -31,6 +32,7 @@ use Mcp\Schema\Tool; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; class RegistryTest extends TestCase @@ -722,6 +724,50 @@ public function load(RegistryInterface $registry): void $this->assertArrayHasKey('loaded', $registry->getTools()->references); } + public function testListChangedEventsAreSuppressedDuringTheDeferredLoad(): void + { + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher->expects($this->never())->method('dispatch'); + + $loader = new class($this->createValidTool('loaded'), $this->createValidResource('file:///loaded'), $this->createValidResourceTemplate('file:///loaded/{id}'), $this->createValidPrompt('loaded_prompt')) implements LoaderInterface { + public function __construct( + private readonly Tool $tool, + private readonly ResourceDefinition $resource, + private readonly ResourceTemplate $template, + private readonly Prompt $prompt, + ) { + } + + public function load(RegistryInterface $registry): void + { + $registry->registerTool($this->tool, 'handler'); + $registry->registerResource($this->resource, 'handler'); + $registry->registerResourceTemplate($this->template, 'handler'); + $registry->registerPrompt($this->prompt, 'handler'); + } + }; + + $registry = new Registry($eventDispatcher, $this->logger, loader: $loader); + + $this->assertTrue($registry->hasTools()); + $this->assertTrue($registry->hasPrompts()); + } + + public function testListChangedEventsAreStillDispatchedForRuntimeRegistrations(): void + { + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher->expects($this->once()) + ->method('dispatch') + ->with($this->isInstanceOf(ToolListChangedEvent::class)) + ->willReturnArgument(0); + + $loader = $this->toolLoader($this->createValidTool('loaded')); + $registry = new Registry($eventDispatcher, $this->logger, loader: $loader); + $registry->load(); + + $registry->registerTool($this->createValidTool('runtime'), 'handler'); + } + public function testLoadRunsTheConfiguredLoaderEagerly(): void { $loader = $this->createMock(LoaderInterface::class);