diff --git a/CHANGELOG.md b/CHANGELOG.md index 37cf6d06..23f55bb4 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 75f2d4a1..bf039736 100644 --- a/src/Capability/Discovery/CachedDiscoverer.php +++ b/src/Capability/Discovery/CachedDiscoverer.php @@ -20,14 +20,16 @@ * 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 74031e56..b880a89d 100644 --- a/src/Capability/Discovery/Discoverer.php +++ b/src/Capability/Discovery/Discoverer.php @@ -42,14 +42,16 @@ * 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 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 125d4bd4..225215dc 100644 --- a/src/Capability/Discovery/SchemaGenerator.php +++ b/src/Capability/Discovery/SchemaGenerator.php @@ -13,6 +13,7 @@ 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 @@ * * @author Kyrian Obikwelu */ -class SchemaGenerator +final class SchemaGenerator implements SchemaGeneratorInterface { public function __construct( private readonly DocBlockParser $docBlockParser, @@ -65,12 +66,20 @@ 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 \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 @@ public function generate(\ReflectionMethod|\ReflectionFunction $reflection): arr * 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 @@ public function generateOutputSchema(\ReflectionMethod|\ReflectionFunction $refl * * @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..c21d3cdd --- /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 f23e5270..cd327b66 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; @@ -84,13 +85,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/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php new file mode 100644 index 00000000..d7c00c14 --- /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 1079c8a4..9e9b6b2f 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 @@ 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(), @@ -470,11 +492,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) { @@ -531,4 +554,15 @@ public function build(): Server 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; + } }