From 7cb29125bdb6f6afd690c901ce3b2c0e9e1cc2e5 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Sun, 11 Jan 2026 22:03:43 +0100 Subject: [PATCH] Add interface abstractions for SchemaGenerator and Discoverer with DI improvements (#215) * schema generator injection * add interface for schema generator * improve discoverer dependency * remove some comments * make some classes final * simplify condition * code review * Extend changelog for covering interfaces * Adopt SchemaGeneratorInterface after rebase and #153 --------- Co-authored-by: Christopher Hertel --- CHANGELOG.md | 1 + src/Capability/Discovery/CachedDiscoverer.php | 6 ++- src/Capability/Discovery/Discoverer.php | 6 ++- .../Discovery/DiscovererInterface.php | 31 ++++++++++++++ src/Capability/Discovery/SchemaGenerator.php | 29 ++++++++++--- .../Discovery/SchemaGeneratorInterface.php | 41 +++++++++++++++++++ .../Registry/Loader/ArrayLoader.php | 4 +- .../Registry/Loader/DiscoveryLoader.php | 19 +++------ src/Exception/BadMethodCallException.php | 19 +++++++++ src/Server/Builder.php | 38 ++++++++++++++++- 10 files changed, 167 insertions(+), 27 deletions(-) create mode 100644 src/Capability/Discovery/DiscovererInterface.php create mode 100644 src/Capability/Discovery/SchemaGeneratorInterface.php create mode 100644 src/Exception/BadMethodCallException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 37cf6d0..23f55bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * Add output schema support to MCP tools * Add validation of the input parameters given to a Tool. * Rename `Mcp\Capability\Registry\ResourceReference::$schema` to `Mcp\Capability\Registry\ResourceReference::$resource`. +* Introduce `SchemaGeneratorInterface` and `DiscovererInterface` to allow custom schema generation and discovery implementations. 0.2.2 ----- diff --git a/src/Capability/Discovery/CachedDiscoverer.php b/src/Capability/Discovery/CachedDiscoverer.php index 75f2d4a..bf03973 100644 --- a/src/Capability/Discovery/CachedDiscoverer.php +++ b/src/Capability/Discovery/CachedDiscoverer.php @@ -20,14 +20,16 @@ use Psr\SimpleCache\CacheInterface; * This decorator caches the results of file system operations and reflection * to improve performance when discovery is called multiple times. * + * @internal + * * @author Xentixar */ -class CachedDiscoverer +final 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 74031e5..b880a89 100644 --- a/src/Capability/Discovery/Discoverer.php +++ b/src/Capability/Discovery/Discoverer.php @@ -42,14 +42,16 @@ use Symfony\Component\Finder\SplFileInfo; * resourceTemplates: int, * } * + * @internal + * * @author Kyrian Obikwelu */ -class Discoverer +final 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 0000000..0de02de --- /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 125d4bd..225215d 100644 --- a/src/Capability/Discovery/SchemaGenerator.php +++ b/src/Capability/Discovery/SchemaGenerator.php @@ -13,6 +13,7 @@ namespace Mcp\Capability\Discovery; use Mcp\Capability\Attribute\McpTool; use Mcp\Capability\Attribute\Schema; +use Mcp\Exception\BadMethodCallException; use Mcp\Exception\InvalidArgumentException; use Mcp\Server\RequestContext; use phpDocumentor\Reflection\DocBlock\Tags\Param; @@ -57,7 +58,7 @@ use phpDocumentor\Reflection\DocBlock\Tags\Param; * * @author Kyrian Obikwelu */ -class SchemaGenerator +final class SchemaGenerator implements SchemaGeneratorInterface { public function __construct( private readonly DocBlockParser $docBlockParser, @@ -65,12 +66,20 @@ class SchemaGenerator } /** - * 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 \ReflectionMethod && !$reflection instanceof \ReflectionFunction) { + throw new BadMethodCallException(\sprintf('Schema generation from %s is not supported. Use ReflectionMethod or ReflectionFunction instead.', $reflection::class)); + } + $methodSchema = $this->extractMethodLevelSchema($reflection); if ($methodSchema && isset($methodSchema['definition'])) { @@ -88,10 +97,18 @@ class SchemaGenerator * Only returns an outputSchema if explicitly provided in the McpTool attribute. * Per MCP spec, outputSchema should only be present when explicitly provided. * - * @return array|null + * @return ?array */ - public function generateOutputSchema(\ReflectionMethod|\ReflectionFunction $reflection): ?array + public function generateOutputSchema(\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 \ReflectionMethod && !$reflection instanceof \ReflectionFunction) { + throw new BadMethodCallException(\sprintf('Schema generation from %s is not supported. Use ReflectionMethod or ReflectionFunction instead.', $reflection::class)); + } + // Only return outputSchema if explicitly provided in McpTool attribute $mcpToolAttrs = $reflection->getAttributes(McpTool::class, \ReflectionAttribute::IS_INSTANCEOF); if ($mcpToolAttrs) { @@ -108,7 +125,7 @@ class SchemaGenerator * * @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 0000000..c21d3cd --- /dev/null +++ b/src/Capability/Discovery/SchemaGeneratorInterface.php @@ -0,0 +1,41 @@ + + */ +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; + + /** + * Generates a JSON Schema for output/result. + * + * @return ?array + */ + public function generateOutputSchema(\Reflector $reflection): ?array; +} diff --git a/src/Capability/Registry/Loader/ArrayLoader.php b/src/Capability/Registry/Loader/ArrayLoader.php index f23e527..cd327b6 100644 --- a/src/Capability/Registry/Loader/ArrayLoader.php +++ b/src/Capability/Registry/Loader/ArrayLoader.php @@ -18,6 +18,7 @@ use Mcp\Capability\Completion\ProviderInterface; 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; @@ -84,13 +85,14 @@ final class ArrayLoader implements LoaderInterface 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 6129dfa..25261ff 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 @@ final class DiscoveryLoader implements LoaderInterface 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/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php new file mode 100644 index 0000000..d7c00c1 --- /dev/null +++ b/src/Exception/BadMethodCallException.php @@ -0,0 +1,19 @@ + + */ +final class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface +{ +} diff --git a/src/Server/Builder.php b/src/Server/Builder.php index 1079c8a..9e9b6b2 100644 --- a/src/Server/Builder.php +++ b/src/Server/Builder.php @@ -11,6 +11,10 @@ namespace Mcp\Server; +use Mcp\Capability\Discovery\CachedDiscoverer; +use Mcp\Capability\Discovery\Discoverer; +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 +62,10 @@ final class Builder private ?ContainerInterface $container = null; + private ?SchemaGeneratorInterface $schemaGenerator = null; + + private ?DiscovererInterface $discoverer = null; + private ?SessionFactoryInterface $sessionFactory = null; private ?SessionStoreInterface $sessionStore = null; @@ -287,6 +295,20 @@ final class Builder 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(), @@ -470,11 +492,12 @@ final class Builder $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) { @@ -531,4 +554,15 @@ final class Builder return new Server($protocol, $logger); } + + private function createDiscoverer(LoggerInterface $logger): DiscovererInterface + { + $discoverer = new Discoverer($logger, null, $this->schemaGenerator); + + if (null !== $this->discoveryCache) { + return new CachedDiscoverer($discoverer, $this->discoveryCache, $logger); + } + + return $discoverer; + } }