Skip to content

Commit 7cb2912

Browse files
soyukachr-hertel
andauthored
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>
1 parent 1de4ea9 commit 7cb2912

10 files changed

Lines changed: 167 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
88
* Add output schema support to MCP tools
99
* Add validation of the input parameters given to a Tool.
1010
* Rename `Mcp\Capability\Registry\ResourceReference::$schema` to `Mcp\Capability\Registry\ResourceReference::$resource`.
11+
* Introduce `SchemaGeneratorInterface` and `DiscovererInterface` to allow custom schema generation and discovery implementations.
1112

1213
0.2.2
1314
-----

src/Capability/Discovery/CachedDiscoverer.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@
2020
* This decorator caches the results of file system operations and reflection
2121
* to improve performance when discovery is called multiple times.
2222
*
23+
* @internal
24+
*
2325
* @author Xentixar <xentixar@gmail.com>
2426
*/
25-
class CachedDiscoverer
27+
final class CachedDiscoverer implements DiscovererInterface
2628
{
2729
private const CACHE_PREFIX = 'mcp_discovery_';
2830

2931
public function __construct(
30-
private readonly Discoverer $discoverer,
32+
private readonly DiscovererInterface $discoverer,
3133
private readonly CacheInterface $cache,
3234
private readonly LoggerInterface $logger,
3335
) {

src/Capability/Discovery/Discoverer.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,16 @@
4242
* resourceTemplates: int,
4343
* }
4444
*
45+
* @internal
46+
*
4547
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
4648
*/
47-
class Discoverer
49+
final class Discoverer implements DiscovererInterface
4850
{
4951
public function __construct(
5052
private readonly LoggerInterface $logger = new NullLogger(),
5153
private ?DocBlockParser $docBlockParser = null,
52-
private ?SchemaGenerator $schemaGenerator = null,
54+
private ?SchemaGeneratorInterface $schemaGenerator = null,
5355
) {
5456
$this->docBlockParser = $docBlockParser ?? new DocBlockParser(logger: $this->logger);
5557
$this->schemaGenerator = $schemaGenerator ?? new SchemaGenerator($this->docBlockParser);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Capability\Discovery;
13+
14+
/**
15+
* Discovers MCP elements (tools, resources, prompts, resource templates) in directories.
16+
*
17+
* @internal
18+
*
19+
* @author Antoine Bluchet <soyuka@gmail.com>
20+
*/
21+
interface DiscovererInterface
22+
{
23+
/**
24+
* Discover MCP elements in the specified directories and return the discovery state.
25+
*
26+
* @param string $basePath the base path for resolving directories
27+
* @param array<string> $directories list of directories (relative to base path) to scan
28+
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
29+
*/
30+
public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState;
31+
}

src/Capability/Discovery/SchemaGenerator.php

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Mcp\Capability\Attribute\McpTool;
1515
use Mcp\Capability\Attribute\Schema;
16+
use Mcp\Exception\BadMethodCallException;
1617
use Mcp\Exception\InvalidArgumentException;
1718
use Mcp\Server\RequestContext;
1819
use phpDocumentor\Reflection\DocBlock\Tags\Param;
@@ -57,20 +58,28 @@
5758
*
5859
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
5960
*/
60-
class SchemaGenerator
61+
final class SchemaGenerator implements SchemaGeneratorInterface
6162
{
6263
public function __construct(
6364
private readonly DocBlockParser $docBlockParser,
6465
) {
6566
}
6667

6768
/**
68-
* Generates a JSON Schema object (as a PHP array) for a method's or function's parameters.
69+
* Generates a JSON Schema object (as a PHP array) for parameters.
6970
*
7071
* @return array<string, mixed>
7172
*/
72-
public function generate(\ReflectionMethod|\ReflectionFunction $reflection): array
73+
public function generate(\Reflector $reflection): array
7374
{
75+
if ($reflection instanceof \ReflectionClass) {
76+
throw new BadMethodCallException('Schema generation from ReflectionClass is not implemented yet. Use ReflectionMethod or ReflectionFunction instead.');
77+
}
78+
79+
if (!$reflection instanceof \ReflectionMethod && !$reflection instanceof \ReflectionFunction) {
80+
throw new BadMethodCallException(\sprintf('Schema generation from %s is not supported. Use ReflectionMethod or ReflectionFunction instead.', $reflection::class));
81+
}
82+
7483
$methodSchema = $this->extractMethodLevelSchema($reflection);
7584

7685
if ($methodSchema && isset($methodSchema['definition'])) {
@@ -88,10 +97,18 @@ public function generate(\ReflectionMethod|\ReflectionFunction $reflection): arr
8897
* Only returns an outputSchema if explicitly provided in the McpTool attribute.
8998
* Per MCP spec, outputSchema should only be present when explicitly provided.
9099
*
91-
* @return array<string, mixed>|null
100+
* @return ?array<string, mixed>
92101
*/
93-
public function generateOutputSchema(\ReflectionMethod|\ReflectionFunction $reflection): ?array
102+
public function generateOutputSchema(\Reflector $reflection): ?array
94103
{
104+
if ($reflection instanceof \ReflectionClass) {
105+
throw new BadMethodCallException('Schema generation from ReflectionClass is not implemented yet. Use ReflectionMethod or ReflectionFunction instead.');
106+
}
107+
108+
if (!$reflection instanceof \ReflectionMethod && !$reflection instanceof \ReflectionFunction) {
109+
throw new BadMethodCallException(\sprintf('Schema generation from %s is not supported. Use ReflectionMethod or ReflectionFunction instead.', $reflection::class));
110+
}
111+
95112
// Only return outputSchema if explicitly provided in McpTool attribute
96113
$mcpToolAttrs = $reflection->getAttributes(McpTool::class, \ReflectionAttribute::IS_INSTANCEOF);
97114
if ($mcpToolAttrs) {
@@ -108,7 +125,7 @@ public function generateOutputSchema(\ReflectionMethod|\ReflectionFunction $refl
108125
*
109126
* @return SchemaAttributeData
110127
*/
111-
private function extractMethodLevelSchema(\ReflectionMethod|\ReflectionFunction $reflection): ?array
128+
private function extractMethodLevelSchema(\ReflectionFunctionAbstract $reflection): ?array
112129
{
113130
$schemaAttrs = $reflection->getAttributes(Schema::class, \ReflectionAttribute::IS_INSTANCEOF);
114131
if (empty($schemaAttrs)) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Capability\Discovery;
13+
14+
/**
15+
* Provides JSON Schema generation for reflected elements.
16+
*
17+
* @author Antoine Bluchet <soyuka@gmail.com>
18+
*/
19+
interface SchemaGeneratorInterface
20+
{
21+
/**
22+
* Generates a JSON Schema for input parameters.
23+
*
24+
* The returned schema must be a valid JSON Schema object (type: 'object')
25+
* with properties corresponding to a tool's parameters.
26+
*
27+
* @return array{
28+
* type: 'object',
29+
* properties: array<string, mixed>|object,
30+
* required?: string[]
31+
* }
32+
*/
33+
public function generate(\Reflector $reflection): array;
34+
35+
/**
36+
* Generates a JSON Schema for output/result.
37+
*
38+
* @return ?array<string, mixed>
39+
*/
40+
public function generateOutputSchema(\Reflector $reflection): ?array;
41+
}

src/Capability/Registry/Loader/ArrayLoader.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Mcp\Capability\Discovery\DocBlockParser;
1919
use Mcp\Capability\Discovery\HandlerResolver;
2020
use Mcp\Capability\Discovery\SchemaGenerator;
21+
use Mcp\Capability\Discovery\SchemaGeneratorInterface;
2122
use Mcp\Capability\Registry\ElementReference;
2223
use Mcp\Capability\RegistryInterface;
2324
use Mcp\Exception\ConfigurationException;
@@ -84,13 +85,14 @@ public function __construct(
8485
private readonly array $resourceTemplates = [],
8586
private readonly array $prompts = [],
8687
private LoggerInterface $logger = new NullLogger(),
88+
private ?SchemaGeneratorInterface $schemaGenerator = null,
8789
) {
8890
}
8991

9092
public function load(RegistryInterface $registry): void
9193
{
9294
$docBlockParser = new DocBlockParser(logger: $this->logger);
93-
$schemaGenerator = new SchemaGenerator($docBlockParser);
95+
$schemaGenerator = $this->schemaGenerator ?? new SchemaGenerator($docBlockParser);
9496

9597
// Register Tools
9698
foreach ($this->tools as $data) {

src/Capability/Registry/Loader/DiscoveryLoader.php

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111

1212
namespace Mcp\Capability\Registry\Loader;
1313

14-
use Mcp\Capability\Discovery\CachedDiscoverer;
15-
use Mcp\Capability\Discovery\Discoverer;
14+
use Mcp\Capability\Discovery\DiscovererInterface;
1615
use Mcp\Capability\RegistryInterface;
17-
use Psr\Log\LoggerInterface;
18-
use Psr\SimpleCache\CacheInterface;
1916

2017
/**
18+
* @internal
19+
*
2120
* @author Antoine Bluchet <soyuka@gmail.com>
2221
*/
2322
final class DiscoveryLoader implements LoaderInterface
@@ -30,21 +29,13 @@ public function __construct(
3029
private string $basePath,
3130
private array $scanDirs,
3231
private array $excludeDirs,
33-
private LoggerInterface $logger,
34-
private ?CacheInterface $cache = null,
32+
private DiscovererInterface $discoverer,
3533
) {
3634
}
3735

3836
public function load(RegistryInterface $registry): void
3937
{
40-
// This now encapsulates the discovery process
41-
$discoverer = new Discoverer($this->logger);
42-
43-
$cachedDiscoverer = $this->cache
44-
? new CachedDiscoverer($discoverer, $this->cache, $this->logger)
45-
: $discoverer;
46-
47-
$discoveryState = $cachedDiscoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs);
38+
$discoveryState = $this->discoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs);
4839

4940
$registry->setDiscoveryState($discoveryState);
5041
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Exception;
13+
14+
/**
15+
* @author Antoine Bluchet <soyuka@gmail.com>
16+
*/
17+
final class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
18+
{
19+
}

src/Server/Builder.php

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
namespace Mcp\Server;
1313

14+
use Mcp\Capability\Discovery\CachedDiscoverer;
15+
use Mcp\Capability\Discovery\Discoverer;
16+
use Mcp\Capability\Discovery\DiscovererInterface;
17+
use Mcp\Capability\Discovery\SchemaGeneratorInterface;
1418
use Mcp\Capability\Registry;
1519
use Mcp\Capability\Registry\Container;
1620
use Mcp\Capability\Registry\ElementReference;
@@ -58,6 +62,10 @@ final class Builder
5862

5963
private ?ContainerInterface $container = null;
6064

65+
private ?SchemaGeneratorInterface $schemaGenerator = null;
66+
67+
private ?DiscovererInterface $discoverer = null;
68+
6169
private ?SessionFactoryInterface $sessionFactory = null;
6270

6371
private ?SessionStoreInterface $sessionStore = null;
@@ -287,6 +295,20 @@ public function setContainer(ContainerInterface $container): self
287295
return $this;
288296
}
289297

298+
public function setSchemaGenerator(SchemaGeneratorInterface $schemaGenerator): self
299+
{
300+
$this->schemaGenerator = $schemaGenerator;
301+
302+
return $this;
303+
}
304+
305+
public function setDiscoverer(DiscovererInterface $discoverer): self
306+
{
307+
$this->discoverer = $discoverer;
308+
309+
return $this;
310+
}
311+
290312
public function setSession(
291313
SessionStoreInterface $sessionStore,
292314
SessionFactoryInterface $sessionFactory = new SessionFactory(),
@@ -470,11 +492,12 @@ public function build(): Server
470492

471493
$loaders = [
472494
...$this->loaders,
473-
new ArrayLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger),
495+
new ArrayLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger, $this->schemaGenerator),
474496
];
475497

476498
if (null !== $this->discoveryBasePath) {
477-
$loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $logger, $this->discoveryCache);
499+
$discoverer = $this->discoverer ?? $this->createDiscoverer($logger);
500+
$loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $discoverer);
478501
}
479502

480503
foreach ($loaders as $loader) {
@@ -531,4 +554,15 @@ public function build(): Server
531554

532555
return new Server($protocol, $logger);
533556
}
557+
558+
private function createDiscoverer(LoggerInterface $logger): DiscovererInterface
559+
{
560+
$discoverer = new Discoverer($logger, null, $this->schemaGenerator);
561+
562+
if (null !== $this->discoveryCache) {
563+
return new CachedDiscoverer($discoverer, $this->discoveryCache, $logger);
564+
}
565+
566+
return $discoverer;
567+
}
534568
}

0 commit comments

Comments
 (0)