diff --git a/src/Capability/Discovery/CachedDiscoverer.php b/src/Capability/Discovery/CachedDiscoverer.php index 75f2d4a1..15bc1226 100644 --- a/src/Capability/Discovery/CachedDiscoverer.php +++ b/src/Capability/Discovery/CachedDiscoverer.php @@ -20,14 +20,18 @@ * This decorator caches the results of file system operations and reflection * to improve performance when discovery is called multiple times. * + * @internal + * + * @final + * * @author Xentixar */ -class CachedDiscoverer +class CachedDiscoverer implements DiscovererInterface { private const CACHE_PREFIX = 'mcp_discovery_'; public function __construct( - private readonly Discoverer $discoverer, + private readonly DiscovererInterface $discoverer, private readonly CacheInterface $cache, private readonly LoggerInterface $logger, ) { diff --git a/src/Capability/Discovery/Discoverer.php b/src/Capability/Discovery/Discoverer.php index 95a3fe5a..c5367535 100644 --- a/src/Capability/Discovery/Discoverer.php +++ b/src/Capability/Discovery/Discoverer.php @@ -42,14 +42,18 @@ * resourceTemplates: int, * } * + * @internal + * + * @final + * * @author Kyrian Obikwelu */ -class Discoverer +class Discoverer implements DiscovererInterface { public function __construct( private readonly LoggerInterface $logger = new NullLogger(), private ?DocBlockParser $docBlockParser = null, - private ?SchemaGenerator $schemaGenerator = null, + private ?SchemaGeneratorInterface $schemaGenerator = null, ) { $this->docBlockParser = $docBlockParser ?? new DocBlockParser(logger: $this->logger); $this->schemaGenerator = $schemaGenerator ?? new SchemaGenerator($this->docBlockParser); diff --git a/src/Capability/Discovery/DiscovererInterface.php b/src/Capability/Discovery/DiscovererInterface.php new file mode 100644 index 00000000..0de02de4 --- /dev/null +++ b/src/Capability/Discovery/DiscovererInterface.php @@ -0,0 +1,31 @@ + + */ +interface DiscovererInterface +{ + /** + * Discover MCP elements in the specified directories and return the discovery state. + * + * @param string $basePath the base path for resolving directories + * @param array $directories list of directories (relative to base path) to scan + * @param array $excludeDirs list of directories (relative to base path) to exclude from the scan + */ + public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState; +} diff --git a/src/Capability/Discovery/SchemaGenerator.php b/src/Capability/Discovery/SchemaGenerator.php index 0676e8d9..0d52f877 100644 --- a/src/Capability/Discovery/SchemaGenerator.php +++ b/src/Capability/Discovery/SchemaGenerator.php @@ -56,7 +56,7 @@ * * @author Kyrian Obikwelu */ -class SchemaGenerator +class SchemaGenerator implements SchemaGeneratorInterface { public function __construct( private readonly DocBlockParser $docBlockParser, @@ -64,12 +64,24 @@ public function __construct( } /** - * Generates a JSON Schema object (as a PHP array) for a method's or function's parameters. + * Generates a JSON Schema object (as a PHP array) for parameters. * * @return array */ - public function generate(\ReflectionMethod|\ReflectionFunction $reflection): array + public function generate(\Reflector $reflection): array { + if ($reflection instanceof \ReflectionClass) { + throw new \BadMethodCallException('Schema generation from ReflectionClass is not implemented yet. Use ReflectionMethod or ReflectionFunction instead.'); + } + + if (!$reflection instanceof \ReflectionFunctionAbstract) { + throw new \BadMethodCallException(\sprintf('Schema generation from %s is not supported.', $reflection::class)); + } + + if (!$reflection instanceof \ReflectionMethod && !$reflection instanceof \ReflectionFunction) { + throw new \BadMethodCallException(\sprintf('Schema generation from %s is not supported.', $reflection::class)); + } + $methodSchema = $this->extractMethodLevelSchema($reflection); if ($methodSchema && isset($methodSchema['definition'])) { @@ -86,7 +98,7 @@ public function generate(\ReflectionMethod|\ReflectionFunction $reflection): arr * * @return SchemaAttributeData */ - private function extractMethodLevelSchema(\ReflectionMethod|\ReflectionFunction $reflection): ?array + private function extractMethodLevelSchema(\ReflectionFunctionAbstract $reflection): ?array { $schemaAttrs = $reflection->getAttributes(Schema::class, \ReflectionAttribute::IS_INSTANCEOF); if (empty($schemaAttrs)) { diff --git a/src/Capability/Discovery/SchemaGeneratorInterface.php b/src/Capability/Discovery/SchemaGeneratorInterface.php new file mode 100644 index 00000000..6bc2cb6f --- /dev/null +++ b/src/Capability/Discovery/SchemaGeneratorInterface.php @@ -0,0 +1,34 @@ + + */ +interface SchemaGeneratorInterface +{ + /** + * Generates a JSON Schema for input parameters. + * + * The returned schema must be a valid JSON Schema object (type: 'object') + * with properties corresponding to a tool's parameters. + * + * @return array{ + * type: 'object', + * properties: array|object, + * required?: string[] + * } + */ + public function generate(\Reflector $reflection): array; +} diff --git a/src/Capability/Registry/Loader/ArrayLoader.php b/src/Capability/Registry/Loader/ArrayLoader.php index fef17263..b2f67e9a 100644 --- a/src/Capability/Registry/Loader/ArrayLoader.php +++ b/src/Capability/Registry/Loader/ArrayLoader.php @@ -18,6 +18,7 @@ use Mcp\Capability\Discovery\DocBlockParser; use Mcp\Capability\Discovery\HandlerResolver; use Mcp\Capability\Discovery\SchemaGenerator; +use Mcp\Capability\Discovery\SchemaGeneratorInterface; use Mcp\Capability\Registry\ElementReference; use Mcp\Capability\RegistryInterface; use Mcp\Exception\ConfigurationException; @@ -83,13 +84,14 @@ public function __construct( private readonly array $resourceTemplates = [], private readonly array $prompts = [], private LoggerInterface $logger = new NullLogger(), + private ?SchemaGeneratorInterface $schemaGenerator = null, ) { } public function load(RegistryInterface $registry): void { $docBlockParser = new DocBlockParser(logger: $this->logger); - $schemaGenerator = new SchemaGenerator($docBlockParser); + $schemaGenerator = $this->schemaGenerator ?? new SchemaGenerator($docBlockParser); // Register Tools foreach ($this->tools as $data) { diff --git a/src/Capability/Registry/Loader/DiscoveryLoader.php b/src/Capability/Registry/Loader/DiscoveryLoader.php index 6129dfaa..25261ff5 100644 --- a/src/Capability/Registry/Loader/DiscoveryLoader.php +++ b/src/Capability/Registry/Loader/DiscoveryLoader.php @@ -11,13 +11,12 @@ namespace Mcp\Capability\Registry\Loader; -use Mcp\Capability\Discovery\CachedDiscoverer; -use Mcp\Capability\Discovery\Discoverer; +use Mcp\Capability\Discovery\DiscovererInterface; use Mcp\Capability\RegistryInterface; -use Psr\Log\LoggerInterface; -use Psr\SimpleCache\CacheInterface; /** + * @internal + * * @author Antoine Bluchet */ final class DiscoveryLoader implements LoaderInterface @@ -30,21 +29,13 @@ public function __construct( private string $basePath, private array $scanDirs, private array $excludeDirs, - private LoggerInterface $logger, - private ?CacheInterface $cache = null, + private DiscovererInterface $discoverer, ) { } public function load(RegistryInterface $registry): void { - // This now encapsulates the discovery process - $discoverer = new Discoverer($this->logger); - - $cachedDiscoverer = $this->cache - ? new CachedDiscoverer($discoverer, $this->cache, $this->logger) - : $discoverer; - - $discoveryState = $cachedDiscoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs); + $discoveryState = $this->discoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs); $registry->setDiscoveryState($discoveryState); } diff --git a/src/Server/Builder.php b/src/Server/Builder.php index 363a09c5..daabfcd7 100644 --- a/src/Server/Builder.php +++ b/src/Server/Builder.php @@ -11,6 +11,8 @@ namespace Mcp\Server; +use Mcp\Capability\Discovery\DiscovererInterface; +use Mcp\Capability\Discovery\SchemaGeneratorInterface; use Mcp\Capability\Registry; use Mcp\Capability\Registry\Container; use Mcp\Capability\Registry\ElementReference; @@ -58,6 +60,10 @@ final class Builder private ?ContainerInterface $container = null; + private ?SchemaGeneratorInterface $schemaGenerator = null; + + private ?DiscovererInterface $discoverer = null; + private ?SessionFactoryInterface $sessionFactory = null; private ?SessionStoreInterface $sessionStore = null; @@ -286,6 +292,20 @@ public function setContainer(ContainerInterface $container): self return $this; } + public function setSchemaGenerator(SchemaGeneratorInterface $schemaGenerator): self + { + $this->schemaGenerator = $schemaGenerator; + + return $this; + } + + public function setDiscoverer(DiscovererInterface $discoverer): self + { + $this->discoverer = $discoverer; + + return $this; + } + public function setSession( SessionStoreInterface $sessionStore, SessionFactoryInterface $sessionFactory = new SessionFactory(), @@ -466,11 +486,12 @@ public function build(): Server $loaders = [ ...$this->loaders, - new ArrayLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger), + new ArrayLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger, $this->schemaGenerator), ]; if (null !== $this->discoveryBasePath) { - $loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $logger, $this->discoveryCache); + $discoverer = $this->discoverer ?? $this->createDiscoverer($logger); + $loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $discoverer); } foreach ($loaders as $loader) { @@ -527,4 +548,15 @@ public function build(): Server return new Server($protocol, $logger); } + + private function createDiscoverer(LoggerInterface $logger): DiscovererInterface + { + $discoverer = new \Mcp\Capability\Discovery\Discoverer($logger, null, $this->schemaGenerator); + + if (null !== $this->discoveryCache) { + return new \Mcp\Capability\Discovery\CachedDiscoverer($discoverer, $this->discoveryCache, $logger); + } + + return $discoverer; + } }