[Server] Untangle origin tracking from Registry (#302)

Drop the isManual flag from ElementReference and the discovery-state methods
from RegistryInterface. The Registry becomes a flat last-write-wins map;
DiscoveryLoader owns its own owned-set bookkeeping so re-running discovery
only removes elements it itself contributed. Adds ChainLoader for explicit
composition. Manual-over-discovered precedence is preserved via loader
ordering, not a flag on the reference.
This commit is contained in:
Christopher Hertel 2026-05-17 14:03:31 +02:00 • committed by GitHub
commit 967c5cda4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1003 additions and 338 deletions

View file

@ -241,7 +241,7 @@ final class Discoverer implements DiscovererInterface
meta: $instance->meta,
outputSchema: $outputSchema,
);
$tools[$name] = new ToolReference($tool, [$className, $methodName], false);
$tools[$name] = new ToolReference($tool, [$className, $methodName]);
++$discoveredCount['tools'];
break;
@ -259,7 +259,7 @@ final class Discoverer implements DiscovererInterface
$instance->icons,
$instance->meta,
);
$resources[$instance->uri] = new ResourceReference($resource, [$className, $methodName], false);
$resources[$instance->uri] = new ResourceReference($resource, [$className, $methodName]);
++$discoveredCount['resources'];
break;
@ -280,7 +280,7 @@ final class Discoverer implements DiscovererInterface
}
$prompt = new Prompt($name, $instance->title, $description, $arguments, $instance->icons, $instance->meta);
$completionProviders = $this->getCompletionProviders($method);
$prompts[$name] = new PromptReference($prompt, [$className, $methodName], false, $completionProviders);
$prompts[$name] = new PromptReference($prompt, [$className, $methodName], $completionProviders);
++$discoveredCount['prompts'];
break;
@ -293,7 +293,7 @@ final class Discoverer implements DiscovererInterface
$meta = $instance->meta ?? null;
$resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $description, $mimeType, $annotations, $meta);
$completionProviders = $this->getCompletionProviders($method);
$resourceTemplates[$instance->uriTemplate] = new ResourceTemplateReference($resourceTemplate, [$className, $methodName], false, $completionProviders);
$resourceTemplates[$instance->uriTemplate] = new ResourceTemplateReference($resourceTemplate, [$className, $methodName], $completionProviders);
++$discoveredCount['resourceTemplates'];
break;
}

View file

@ -72,6 +72,23 @@ final class DiscoveryState
return $this->resourceTemplates;
}
/**
* Returns the subset of this state whose keys are absent from $next.
*
* Asymmetric by design: entries whose keys exist in both states are excluded
* regardless of value. Used to identify owned entries that a fresh discovery
* no longer produces.
*/
public function obsoletedBy(self $next): self
{
return new self(
array_diff_key($this->tools, $next->tools),
array_diff_key($this->resources, $next->resources),
array_diff_key($this->prompts, $next->prompts),
array_diff_key($this->resourceTemplates, $next->resourceTemplates),
);
}
/**
* Check if this state contains any discovered elements.
*/

View file

@ -11,7 +11,6 @@
namespace Mcp\Capability;
use Mcp\Capability\Discovery\DiscoveryState;
use Mcp\Capability\Registry\PromptReference;
use Mcp\Capability\Registry\ResourceReference;
use Mcp\Capability\Registry\ResourceTemplateReference;
@ -68,129 +67,120 @@ final class Registry implements RegistryInterface
) {
}
public function registerTool(Tool $tool, callable|array|string $handler, bool $isManual = false): void
public function registerTool(Tool $tool, callable|array|string $handler): ToolReference
{
$toolName = $tool->name;
$existing = $this->tools[$toolName] ?? null;
if ($existing && !$isManual && $existing->isManual) {
$this->logger->debug(
\sprintf('Ignoring discovered tool "%s" as it conflicts with a manually registered one.', $toolName),
);
return;
}
if (!$this->nameValidator->isValid($toolName)) {
if (!$this->nameValidator->isValid($tool->name)) {
$this->logger->warning(
\sprintf('Tool name "%s" is invalid. Tool names should only contain letters (a-z, A-Z), numbers, dots, hyphens, underscores, and forward slashes.', $toolName),
\sprintf('Tool name "%s" is invalid. Tool names should only contain letters (a-z, A-Z), numbers, dots, hyphens, underscores, and forward slashes.', $tool->name),
);
}
$this->tools[$toolName] = new ToolReference($tool, $handler, $isManual);
$reference = new ToolReference($tool, $handler);
$this->tools[$tool->name] = $reference;
$this->eventDispatcher?->dispatch(new ToolListChangedEvent());
return $reference;
}
public function registerResource(Resource $resource, callable|array|string $handler, bool $isManual = false): void
public function registerResource(Resource $resource, callable|array|string $handler): ResourceReference
{
$uri = $resource->uri;
$existing = $this->resources[$uri] ?? null;
if ($existing && !$isManual && $existing->isManual) {
$this->logger->debug(
\sprintf('Ignoring discovered resource "%s" as it conflicts with a manually registered one.', $uri),
);
return;
}
$this->resources[$uri] = new ResourceReference($resource, $handler, $isManual);
$reference = new ResourceReference($resource, $handler);
$this->resources[$resource->uri] = $reference;
$this->eventDispatcher?->dispatch(new ResourceListChangedEvent());
return $reference;
}
public function registerResourceTemplate(
ResourceTemplate $template,
callable|array|string $handler,
array $completionProviders = [],
bool $isManual = false,
): void {
$uriTemplate = $template->uriTemplate;
$existing = $this->resourceTemplates[$uriTemplate] ?? null;
if ($existing && !$isManual && $existing->isManual) {
$this->logger->debug(
\sprintf('Ignoring discovered template "%s" as it conflicts with a manually registered one.', $uriTemplate),
);
return;
}
$this->resourceTemplates[$uriTemplate] = new ResourceTemplateReference(
$template,
$handler,
$isManual,
$completionProviders,
);
): ResourceTemplateReference {
$reference = new ResourceTemplateReference($template, $handler, $completionProviders);
$this->resourceTemplates[$template->uriTemplate] = $reference;
$this->eventDispatcher?->dispatch(new ResourceTemplateListChangedEvent());
return $reference;
}
public function registerPrompt(
Prompt $prompt,
callable|array|string $handler,
array $completionProviders = [],
bool $isManual = false,
): void {
$promptName = $prompt->name;
$existing = $this->prompts[$promptName] ?? null;
): PromptReference {
$reference = new PromptReference($prompt, $handler, $completionProviders);
$this->prompts[$prompt->name] = $reference;
if ($existing && !$isManual && $existing->isManual) {
$this->logger->debug(
\sprintf('Ignoring discovered prompt "%s" as it conflicts with a manually registered one.', $promptName),
);
$this->eventDispatcher?->dispatch(new PromptListChangedEvent());
return $reference;
}
public function unregisterTool(string $name): void
{
if (!isset($this->tools[$name])) {
return;
}
$this->prompts[$promptName] = new PromptReference($prompt, $handler, $isManual, $completionProviders);
unset($this->tools[$name]);
$this->eventDispatcher?->dispatch(new ToolListChangedEvent());
}
public function unregisterResource(string $uri): void
{
if (!isset($this->resources[$uri])) {
return;
}
unset($this->resources[$uri]);
$this->eventDispatcher?->dispatch(new ResourceListChangedEvent());
}
public function unregisterResourceTemplate(string $uriTemplate): void
{
if (!isset($this->resourceTemplates[$uriTemplate])) {
return;
}
unset($this->resourceTemplates[$uriTemplate]);
$this->eventDispatcher?->dispatch(new ResourceTemplateListChangedEvent());
}
public function unregisterPrompt(string $name): void
{
if (!isset($this->prompts[$name])) {
return;
}
unset($this->prompts[$name]);
$this->eventDispatcher?->dispatch(new PromptListChangedEvent());
}
public function clear(): void
public function hasTool(string $name): bool
{
$clearCount = 0;
return isset($this->tools[$name]);
}
foreach ($this->tools as $name => $tool) {
if (!$tool->isManual) {
unset($this->tools[$name]);
++$clearCount;
}
}
foreach ($this->resources as $uri => $resource) {
if (!$resource->isManual) {
unset($this->resources[$uri]);
++$clearCount;
}
}
foreach ($this->prompts as $name => $prompt) {
if (!$prompt->isManual) {
unset($this->prompts[$name]);
++$clearCount;
}
}
foreach ($this->resourceTemplates as $uriTemplate => $template) {
if (!$template->isManual) {
unset($this->resourceTemplates[$uriTemplate]);
++$clearCount;
}
}
public function hasResource(string $uri): bool
{
return isset($this->resources[$uri]);
}
if ($clearCount > 0) {
$this->logger->debug(\sprintf('Removed %d discovered elements from internal registry.', $clearCount));
}
public function hasResourceTemplate(string $uriTemplate): bool
{
return isset($this->resourceTemplates[$uriTemplate]);
}
public function hasPrompt(string $name): bool
{
return isset($this->prompts[$name]);
}
public function hasTools(): bool
@ -338,59 +328,6 @@ final class Registry implements RegistryInterface
return $this->prompts[$name] ?? throw new PromptNotFoundException($name);
}
/**
* Get the current discovery state (only discovered elements, not manual ones).
*/
public function getDiscoveryState(): DiscoveryState
{
return new DiscoveryState(
tools: array_filter($this->tools, static fn ($tool) => !$tool->isManual),
resources: array_filter($this->resources, static fn ($resource) => !$resource->isManual),
prompts: array_filter($this->prompts, static fn ($prompt) => !$prompt->isManual),
resourceTemplates: array_filter($this->resourceTemplates, static fn ($template) => !$template->isManual),
);
}
/**
* Set the discovery state, replacing all discovered elements.
* Manual elements are preserved.
*/
public function setDiscoveryState(DiscoveryState $state): void
{
// Clear existing discovered elements
$this->clear();
// Import new discovered elements
foreach ($state->getTools() as $name => $tool) {
$this->tools[$name] = $tool;
}
foreach ($state->getResources() as $uri => $resource) {
$this->resources[$uri] = $resource;
}
foreach ($state->getPrompts() as $name => $prompt) {
$this->prompts[$name] = $prompt;
}
foreach ($state->getResourceTemplates() as $uriTemplate => $template) {
$this->resourceTemplates[$uriTemplate] = $template;
}
// Dispatch events for the imported elements
if ($this->eventDispatcher instanceof EventDispatcherInterface) {
if (!empty($state->getTools())) {
$this->eventDispatcher->dispatch(new ToolListChangedEvent());
}
if (!empty($state->getResources()) || !empty($state->getResourceTemplates())) {
$this->eventDispatcher->dispatch(new ResourceListChangedEvent());
}
if (!empty($state->getPrompts())) {
$this->eventDispatcher->dispatch(new PromptListChangedEvent());
}
}
}
/**
* Calculate next cursor for pagination.
*

View file

@ -23,7 +23,6 @@ class ElementReference
*/
public function __construct(
public readonly \Closure|array|string $handler,
public readonly bool $isManual = false,
) {
}
}

View file

@ -124,7 +124,7 @@ final class ArrayLoader implements LoaderInterface
meta: $data['meta'] ?? null,
outputSchema: $data['outputSchema'] ?? null,
);
$registry->registerTool($tool, $data['handler'], true);
$registry->registerTool($tool, $data['handler']);
$handlerDesc = $this->getHandlerDescription($data['handler']);
$this->logger->debug("Registered manual tool {$name} from handler {$handlerDesc}");
@ -164,7 +164,7 @@ final class ArrayLoader implements LoaderInterface
icons: $data['icons'] ?? null,
meta: $data['meta'] ?? null,
);
$registry->registerResource($resource, $data['handler'], true);
$registry->registerResource($resource, $data['handler']);
$handlerDesc = $this->getHandlerDescription($data['handler']);
$this->logger->debug("Registered manual resource {$name} from handler {$handlerDesc}");
@ -203,7 +203,7 @@ final class ArrayLoader implements LoaderInterface
meta: $data['meta'] ?? null,
);
$completionProviders = $this->getCompletionProviders($reflection);
$registry->registerResourceTemplate($template, $data['handler'], $completionProviders, true);
$registry->registerResourceTemplate($template, $data['handler'], $completionProviders);
$handlerDesc = $this->getHandlerDescription($data['handler']);
$this->logger->debug("Registered manual template {$name} from handler {$handlerDesc}");
@ -261,7 +261,7 @@ final class ArrayLoader implements LoaderInterface
meta: $data['meta'] ?? null
);
$completionProviders = $this->getCompletionProviders($reflection);
$registry->registerPrompt($prompt, $data['handler'], $completionProviders, true);
$registry->registerPrompt($prompt, $data['handler'], $completionProviders);
$handlerDesc = $this->getHandlerDescription($data['handler']);
$this->logger->debug("Registered manual prompt {$name} from handler {$handlerDesc}");

View file

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mcp\Capability\Registry\Loader;
use Mcp\Capability\RegistryInterface;
/**
* Composes multiple loaders into a single one. Child loaders run in the order they were given;
* for conflicting keys, the loader that runs later wins (last-write-wins).
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class ChainLoader implements LoaderInterface
{
/**
* @param LoaderInterface[] $loaders
*/
public function __construct(
private readonly array $loaders,
) {
}
public function load(RegistryInterface $registry): void
{
foreach ($this->loaders as $loader) {
$loader->load($registry);
}
}
}

View file

@ -12,7 +12,10 @@
namespace Mcp\Capability\Registry\Loader;
use Mcp\Capability\Discovery\DiscovererInterface;
use Mcp\Capability\Discovery\DiscoveryState;
use Mcp\Capability\RegistryInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* @internal
@ -21,10 +24,12 @@ use Mcp\Capability\RegistryInterface;
*/
final class DiscoveryLoader implements LoaderInterface
{
private DiscoveryState $owned;
/**
* @param string[] $scanDirs
* @param array|string[] $excludeDirs
* @param string[] $namePatterns
* @param string[] $scanDirs
* @param string[] $excludeDirs
* @param string[] $namePatterns
*/
public function __construct(
private string $basePath,
@ -32,13 +37,149 @@ final class DiscoveryLoader implements LoaderInterface
private array $excludeDirs,
private DiscovererInterface $discoverer,
private array $namePatterns = DiscovererInterface::DEFAULT_NAME_PATERNS,
private LoggerInterface $logger = new NullLogger(),
) {
$this->owned = new DiscoveryState();
}
public function load(RegistryInterface $registry): void
{
$discoveryState = $this->discoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs, $this->namePatterns);
$discovered = $this->discoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs, $this->namePatterns);
$registry->setDiscoveryState($discoveryState);
$this->unregisterOwned($registry, $this->owned->obsoletedBy($discovered));
$this->owned = $this->writeDiscovered($registry, $discovered);
}
/**
* Unregisters entries we previously wrote that the registry still attributes to us.
* Entries overwritten by someone else are left untouched (identity check fails).
*/
private function unregisterOwned(RegistryInterface $registry, DiscoveryState $obsolete): void
{
foreach ($obsolete->getTools() as $name => $owned) {
if ($registry->hasTool($name) && $registry->getTool($name) === $owned) {
$registry->unregisterTool($name);
}
}
foreach ($obsolete->getResources() as $uri => $owned) {
if ($registry->hasResource($uri) && $registry->getResource($uri, false) === $owned) {
$registry->unregisterResource($uri);
}
}
foreach ($obsolete->getResourceTemplates() as $uriTemplate => $owned) {
if ($registry->hasResourceTemplate($uriTemplate) && $registry->getResourceTemplate($uriTemplate) === $owned) {
$registry->unregisterResourceTemplate($uriTemplate);
}
}
foreach ($obsolete->getPrompts() as $name => $owned) {
if ($registry->hasPrompt($name) && $registry->getPrompt($name) === $owned) {
$registry->unregisterPrompt($name);
}
}
}
/**
* Writes the discovered state into the registry, skipping entries that a conflicting
* registration already holds. Returns the new owned state (only the writes we actually performed).
*/
private function writeDiscovered(RegistryInterface $registry, DiscoveryState $discovered): DiscoveryState
{
$tools = [];
foreach ($discovered->getTools() as $name => $reference) {
if (!$this->mayWriteTool($registry, $name)) {
continue;
}
$tools[$name] = $registry->registerTool($reference->tool, $reference->handler);
}
$resources = [];
foreach ($discovered->getResources() as $uri => $reference) {
if (!$this->mayWriteResource($registry, $uri)) {
continue;
}
$resources[$uri] = $registry->registerResource($reference->resource, $reference->handler);
}
$resourceTemplates = [];
foreach ($discovered->getResourceTemplates() as $uriTemplate => $reference) {
if (!$this->mayWriteResourceTemplate($registry, $uriTemplate)) {
continue;
}
$resourceTemplates[$uriTemplate] = $registry->registerResourceTemplate(
$reference->resourceTemplate,
$reference->handler,
$reference->completionProviders,
);
}
$prompts = [];
foreach ($discovered->getPrompts() as $name => $reference) {
if (!$this->mayWritePrompt($registry, $name)) {
continue;
}
$prompts[$name] = $registry->registerPrompt(
$reference->prompt,
$reference->handler,
$reference->completionProviders,
);
}
return new DiscoveryState($tools, $resources, $prompts, $resourceTemplates);
}
private function mayWriteTool(RegistryInterface $registry, string $name): bool
{
if (!$registry->hasTool($name) || $registry->getTool($name) === ($this->owned->getTools()[$name] ?? null)) {
return true;
}
$this->logger->debug(\sprintf(
'Ignoring discovered tool "%s": a conflicting manual or runtime registration already exists.',
$name,
));
return false;
}
private function mayWriteResource(RegistryInterface $registry, string $uri): bool
{
if (!$registry->hasResource($uri) || $registry->getResource($uri, false) === ($this->owned->getResources()[$uri] ?? null)) {
return true;
}
$this->logger->debug(\sprintf(
'Ignoring discovered resource "%s": a conflicting manual or runtime registration already exists.',
$uri,
));
return false;
}
private function mayWriteResourceTemplate(RegistryInterface $registry, string $uriTemplate): bool
{
if (!$registry->hasResourceTemplate($uriTemplate) || $registry->getResourceTemplate($uriTemplate) === ($this->owned->getResourceTemplates()[$uriTemplate] ?? null)) {
return true;
}
$this->logger->debug(\sprintf(
'Ignoring discovered resource template "%s": a conflicting manual or runtime registration already exists.',
$uriTemplate,
));
return false;
}
private function mayWritePrompt(RegistryInterface $registry, string $name): bool
{
if (!$registry->hasPrompt($name) || $registry->getPrompt($name) === ($this->owned->getPrompts()[$name] ?? null)) {
return true;
}
$this->logger->debug(\sprintf(
'Ignoring discovered prompt "%s": a conflicting manual or runtime registration already exists.',
$name,
));
return false;
}
}

View file

@ -29,10 +29,9 @@ class PromptReference extends ElementReference
public function __construct(
public readonly Prompt $prompt,
\Closure|array|string $handler,
bool $isManual = false,
public readonly array $completionProviders = [],
) {
parent::__construct($handler, $isManual);
parent::__construct($handler);
}
/**

View file

@ -28,9 +28,8 @@ class ResourceReference extends ElementReference
public function __construct(
public readonly Resource $resource,
callable|array|string $handler,
bool $isManual = false,
) {
parent::__construct($handler, $isManual);
parent::__construct($handler);
}
/**

View file

@ -36,10 +36,9 @@ class ResourceTemplateReference extends ElementReference
public function __construct(
public readonly ResourceTemplate $resourceTemplate,
callable|array|string $handler,
bool $isManual = false,
public readonly array $completionProviders = [],
) {
parent::__construct($handler, $isManual);
parent::__construct($handler);
$this->compileTemplate();
}

View file

@ -28,9 +28,8 @@ class ToolReference extends ElementReference
public function __construct(
public readonly Tool $tool,
callable|array|string $handler,
bool $isManual = false,
) {
parent::__construct($handler, $isManual);
parent::__construct($handler);
}
/**

View file

@ -11,7 +11,6 @@
namespace Mcp\Capability;
use Mcp\Capability\Discovery\DiscoveryState;
use Mcp\Capability\Registry\ElementReference;
use Mcp\Capability\Registry\PromptReference;
use Mcp\Capability\Registry\ResourceReference;
@ -35,21 +34,25 @@ use Mcp\Schema\Tool;
interface RegistryInterface
{
/**
* Registers a tool with its handler.
* Registers a tool with its handler. Overwrites any prior registration of the same name.
* Returns the stored reference, whose identity callers may track to detect later overwrites.
*
* @param Handler $handler
*/
public function registerTool(Tool $tool, callable|array|string $handler, bool $isManual = false): void;
public function registerTool(Tool $tool, callable|array|string $handler): ToolReference;
/**
* Registers a resource with its handler.
* Registers a resource with its handler. Overwrites any prior registration of the same URI.
* Returns the stored reference, whose identity callers may track to detect later overwrites.
*
* @param Handler $handler
*/
public function registerResource(Resource $resource, callable|array|string $handler, bool $isManual = false): void;
public function registerResource(Resource $resource, callable|array|string $handler): ResourceReference;
/**
* Registers a resource template with its handler and completion providers.
* Overwrites any prior registration of the same URI template.
* Returns the stored reference, whose identity callers may track to detect later overwrites.
*
* @param Handler $handler
* @param array<string, class-string|object> $completionProviders
@ -58,11 +61,12 @@ interface RegistryInterface
ResourceTemplate $template,
callable|array|string $handler,
array $completionProviders = [],
bool $isManual = false,
): void;
): ResourceTemplateReference;
/**
* Registers a prompt with its handler and completion providers.
* Overwrites any prior registration of the same name.
* Returns the stored reference, whose identity callers may track to detect later overwrites.
*
* @param Handler $handler
* @param array<string, class-string|object> $completionProviders
@ -71,24 +75,35 @@ interface RegistryInterface
Prompt $prompt,
callable|array|string $handler,
array $completionProviders = [],
bool $isManual = false,
): void;
): PromptReference;
/**
* Clear discovered elements from registry.
* Removes a tool by name. No-op if absent.
*/
public function clear(): void;
public function unregisterTool(string $name): void;
/**
* Get the current discovery state (only discovered elements, not manual ones).
* Removes a resource by URI. No-op if absent.
*/
public function getDiscoveryState(): DiscoveryState;
public function unregisterResource(string $uri): void;
/**
* Set discovery state, replacing all discovered elements.
* Manual elements are preserved.
* Removes a resource template by URI template. No-op if absent.
*/
public function setDiscoveryState(DiscoveryState $state): void;
public function unregisterResourceTemplate(string $uriTemplate): void;
/**
* Removes a prompt by name. No-op if absent.
*/
public function unregisterPrompt(string $name): void;
public function hasTool(string $name): bool;
public function hasResource(string $uri): bool;
public function hasResourceTemplate(string $uriTemplate): bool;
public function hasPrompt(string $name): bool;
/**
* @return bool true if any tools are registered

View file

@ -19,6 +19,7 @@ use Mcp\Capability\Registry;
use Mcp\Capability\Registry\Container;
use Mcp\Capability\Registry\ElementReference;
use Mcp\Capability\Registry\Loader\ArrayLoader;
use Mcp\Capability\Registry\Loader\ChainLoader;
use Mcp\Capability\Registry\Loader\DiscoveryLoader;
use Mcp\Capability\Registry\Loader\LoaderInterface;
use Mcp\Capability\Registry\ReferenceHandler;
@ -540,11 +541,6 @@ final class Builder
$container = $this->container ?? new Container();
$registry = $this->registry ?? new Registry($this->eventDispatcher, $logger);
$subscriptionManager = $this->subscriptionManager ?? new SessionSubscriptionManager($logger);
$loaders = [
...$this->loaders,
new ArrayLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger, $this->schemaGenerator),
];
$sessionManager = $this->sessionManager ?? new SessionManager(
$this->sessionStore ?? new InMemorySessionStore(),
$logger,
@ -552,18 +548,24 @@ final class Builder
$this->gcDivisor,
);
// ArrayLoader runs before DiscoveryLoader so manual entries are seen first; DiscoveryLoader's
// identity check then preserves them against same-name discovered entries.
$loaders = [
...$this->loaders,
new ArrayLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger, $this->schemaGenerator),
];
if (null !== $this->discoveryBasePath) {
if (null !== $this->discoverer || class_exists(Finder::class)) {
$discoverer = $this->discoverer ?? $this->createDiscoverer($logger);
$loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $discoverer, $this->discoveryNamePatterns);
$loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $discoverer, $this->discoveryNamePatterns, $logger);
} else {
$logger->warning('File-based discovery requires symfony/finder. Skipping automatic discovery. Run: composer require symfony/finder');
}
}
foreach ($loaders as $loader) {
$loader->load($registry);
}
$loader = new ChainLoader($loaders);
$loader->load($registry);
$messageFactory = MessageFactory::make();