[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:
parent
e00450a218
commit
967c5cda4d
8 changed files with 1003 additions and 338 deletions
|
|
@ -23,7 +23,6 @@ class ElementReference
|
|||
*/
|
||||
public function __construct(
|
||||
public readonly \Closure|array|string $handler,
|
||||
public readonly bool $isManual = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
38
src/Capability/Registry/Loader/ChainLoader.php
Normal file
38
src/Capability/Registry/Loader/ChainLoader.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue