[Capability] Suppress list-changed events during the initial registry load (#480)
* [Capability] Suppress list-changed events during the initial registry load * Shorten dispatch() comment to a one-liner * Use multi-line docblock format for dispatch() comment
This commit is contained in:
parent
c36e805086
commit
312c621417
2 changed files with 66 additions and 8 deletions
|
|
@ -108,7 +108,7 @@ final class Registry implements RegistryInterface
|
||||||
$reference = new ToolReference($tool, $handler);
|
$reference = new ToolReference($tool, $handler);
|
||||||
$this->tools[$tool->name] = $reference;
|
$this->tools[$tool->name] = $reference;
|
||||||
|
|
||||||
$this->eventDispatcher?->dispatch(new ToolListChangedEvent());
|
$this->dispatch(new ToolListChangedEvent());
|
||||||
|
|
||||||
return $reference;
|
return $reference;
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +118,7 @@ final class Registry implements RegistryInterface
|
||||||
$reference = new ResourceReference($resource, $handler);
|
$reference = new ResourceReference($resource, $handler);
|
||||||
$this->resources[$resource->uri] = $reference;
|
$this->resources[$resource->uri] = $reference;
|
||||||
|
|
||||||
$this->eventDispatcher?->dispatch(new ResourceListChangedEvent());
|
$this->dispatch(new ResourceListChangedEvent());
|
||||||
|
|
||||||
return $reference;
|
return $reference;
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +131,7 @@ final class Registry implements RegistryInterface
|
||||||
$reference = new ResourceTemplateReference($template, $handler, $completionProviders);
|
$reference = new ResourceTemplateReference($template, $handler, $completionProviders);
|
||||||
$this->resourceTemplates[$template->uriTemplate] = $reference;
|
$this->resourceTemplates[$template->uriTemplate] = $reference;
|
||||||
|
|
||||||
$this->eventDispatcher?->dispatch(new ResourceTemplateListChangedEvent());
|
$this->dispatch(new ResourceTemplateListChangedEvent());
|
||||||
|
|
||||||
return $reference;
|
return $reference;
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +144,7 @@ final class Registry implements RegistryInterface
|
||||||
$reference = new PromptReference($prompt, $handler, $completionProviders);
|
$reference = new PromptReference($prompt, $handler, $completionProviders);
|
||||||
$this->prompts[$prompt->name] = $reference;
|
$this->prompts[$prompt->name] = $reference;
|
||||||
|
|
||||||
$this->eventDispatcher?->dispatch(new PromptListChangedEvent());
|
$this->dispatch(new PromptListChangedEvent());
|
||||||
|
|
||||||
return $reference;
|
return $reference;
|
||||||
}
|
}
|
||||||
|
|
@ -157,7 +157,7 @@ final class Registry implements RegistryInterface
|
||||||
|
|
||||||
unset($this->tools[$name]);
|
unset($this->tools[$name]);
|
||||||
|
|
||||||
$this->eventDispatcher?->dispatch(new ToolListChangedEvent());
|
$this->dispatch(new ToolListChangedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function unregisterResource(string $uri): void
|
public function unregisterResource(string $uri): void
|
||||||
|
|
@ -168,7 +168,7 @@ final class Registry implements RegistryInterface
|
||||||
|
|
||||||
unset($this->resources[$uri]);
|
unset($this->resources[$uri]);
|
||||||
|
|
||||||
$this->eventDispatcher?->dispatch(new ResourceListChangedEvent());
|
$this->dispatch(new ResourceListChangedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function unregisterResourceTemplate(string $uriTemplate): void
|
public function unregisterResourceTemplate(string $uriTemplate): void
|
||||||
|
|
@ -179,7 +179,7 @@ final class Registry implements RegistryInterface
|
||||||
|
|
||||||
unset($this->resourceTemplates[$uriTemplate]);
|
unset($this->resourceTemplates[$uriTemplate]);
|
||||||
|
|
||||||
$this->eventDispatcher?->dispatch(new ResourceTemplateListChangedEvent());
|
$this->dispatch(new ResourceTemplateListChangedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function unregisterPrompt(string $name): void
|
public function unregisterPrompt(string $name): void
|
||||||
|
|
@ -190,7 +190,7 @@ final class Registry implements RegistryInterface
|
||||||
|
|
||||||
unset($this->prompts[$name]);
|
unset($this->prompts[$name]);
|
||||||
|
|
||||||
$this->eventDispatcher?->dispatch(new PromptListChangedEvent());
|
$this->dispatch(new PromptListChangedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasTool(string $name): bool
|
public function hasTool(string $name): bool
|
||||||
|
|
@ -390,6 +390,18 @@ final class Registry implements RegistryInterface
|
||||||
return $this->prompts[$name] ?? throw new PromptNotFoundException($name);
|
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.
|
* Calculate next cursor for pagination.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use Mcp\Capability\Registry\ResourceReference;
|
||||||
use Mcp\Capability\Registry\ResourceTemplateReference;
|
use Mcp\Capability\Registry\ResourceTemplateReference;
|
||||||
use Mcp\Capability\Registry\ToolReference;
|
use Mcp\Capability\Registry\ToolReference;
|
||||||
use Mcp\Capability\RegistryInterface;
|
use Mcp\Capability\RegistryInterface;
|
||||||
|
use Mcp\Event\ToolListChangedEvent;
|
||||||
use Mcp\Exception\PromptNotFoundException;
|
use Mcp\Exception\PromptNotFoundException;
|
||||||
use Mcp\Exception\ResourceNotFoundException;
|
use Mcp\Exception\ResourceNotFoundException;
|
||||||
use Mcp\Exception\ToolNotFoundException;
|
use Mcp\Exception\ToolNotFoundException;
|
||||||
|
|
@ -31,6 +32,7 @@ use Mcp\Schema\ResourceTemplate;
|
||||||
use Mcp\Schema\Tool;
|
use Mcp\Schema\Tool;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class RegistryTest extends TestCase
|
class RegistryTest extends TestCase
|
||||||
|
|
@ -722,6 +724,50 @@ class RegistryTest extends TestCase
|
||||||
$this->assertArrayHasKey('loaded', $registry->getTools()->references);
|
$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
|
public function testLoadRunsTheConfiguredLoaderEagerly(): void
|
||||||
{
|
{
|
||||||
$loader = $this->createMock(LoaderInterface::class);
|
$loader = $this->createMock(LoaderInterface::class);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue