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:
Antoine Bluchet 2026-01-11 22:03:43 +01:00 • committed by GitHub
commit 7cb29125bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 167 additions and 27 deletions

View file

@ -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
-----

View file

@ -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,
) {

View file

@ -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);

View 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;
}

View file

@ -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)) {

View 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;
}

View file

@ -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) {

View file

@ -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);
}

View 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
{
}

View file

@ -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;
}
}