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 <mail@christopher-hertel.de>
This commit is contained in:
parent
1de4ea933f
commit
7cb29125bd
10 changed files with 167 additions and 27 deletions
|
|
@ -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
|
||||
-----
|
||||
|
|
|
|||
|
|
@ -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 <xentixar@gmail.com>
|
||||
*/
|
||||
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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -42,14 +42,16 @@ use Symfony\Component\Finder\SplFileInfo;
|
|||
* resourceTemplates: int,
|
||||
* }
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
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);
|
||||
|
|
|
|||
31
src/Capability/Discovery/DiscovererInterface.php
Normal file
31
src/Capability/Discovery/DiscovererInterface.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?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\Discovery;
|
||||
|
||||
/**
|
||||
* Discovers MCP elements (tools, resources, prompts, resource templates) in directories.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
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<string> $directories list of directories (relative to base path) to scan
|
||||
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
|
||||
*/
|
||||
public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState;
|
||||
}
|
||||
|
|
@ -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 <koshnawaza@gmail.com>
|
||||
*/
|
||||
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<string, mixed>
|
||||
*/
|
||||
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<string, mixed>|null
|
||||
* @return ?array<string, mixed>
|
||||
*/
|
||||
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)) {
|
||||
|
|
|
|||
41
src/Capability/Discovery/SchemaGeneratorInterface.php
Normal file
41
src/Capability/Discovery/SchemaGeneratorInterface.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?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\Discovery;
|
||||
|
||||
/**
|
||||
* Provides JSON Schema generation for reflected elements.
|
||||
*
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
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<string, mixed>|object,
|
||||
* required?: string[]
|
||||
* }
|
||||
*/
|
||||
public function generate(\Reflector $reflection): array;
|
||||
|
||||
/**
|
||||
* Generates a JSON Schema for output/result.
|
||||
*
|
||||
* @return ?array<string, mixed>
|
||||
*/
|
||||
public function generateOutputSchema(\Reflector $reflection): ?array;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 <soyuka@gmail.com>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
19
src/Exception/BadMethodCallException.php
Normal file
19
src/Exception/BadMethodCallException.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
*/
|
||||
final class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue