Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/Capability/Discovery/CachedDiscoverer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@
* This decorator caches the results of file system operations and reflection
* to improve performance when discovery is called multiple times.
*
* @internal
*
* @final
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should I mark the class final directly?

*
* @author Xentixar <xentixar@gmail.com>
*/
class CachedDiscoverer
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,
) {
Expand Down
8 changes: 6 additions & 2 deletions src/Capability/Discovery/Discoverer.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,18 @@
* resourceTemplates: int,
* }
*
* @internal
*
* @final
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
class Discoverer
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);
Expand Down
31 changes: 31 additions & 0 deletions src/Capability/Discovery/DiscovererInterface.php
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 16 additions & 4 deletions src/Capability/Discovery/SchemaGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,32 @@
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
class SchemaGenerator
class SchemaGenerator implements SchemaGeneratorInterface
{
public function __construct(
private readonly DocBlockParser $docBlockParser,
) {
}

/**
* 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 \ReflectionFunctionAbstract) {
throw new \BadMethodCallException(\sprintf('Schema generation from %s is not supported.', $reflection::class));
}

if (!$reflection instanceof \ReflectionMethod && !$reflection instanceof \ReflectionFunction) {
throw new \BadMethodCallException(\sprintf('Schema generation from %s is not supported.', $reflection::class));
}

$methodSchema = $this->extractMethodLevelSchema($reflection);

if ($methodSchema && isset($methodSchema['definition'])) {
Expand All @@ -86,7 +98,7 @@ public function generate(\ReflectionMethod|\ReflectionFunction $reflection): arr
*
* @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)) {
Expand Down
34 changes: 34 additions & 0 deletions src/Capability/Discovery/SchemaGeneratorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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;
}
4 changes: 3 additions & 1 deletion src/Capability/Registry/Loader/ArrayLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -83,13 +84,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) {
Expand Down
18 changes: 4 additions & 14 deletions src/Capability/Registry/Loader/DiscoveryLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@

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
Expand All @@ -30,21 +28,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);
}
Expand Down
36 changes: 34 additions & 2 deletions src/Server/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Mcp\Server;

use Mcp\Capability\Discovery\DiscovererInterface;
use Mcp\Capability\Discovery\SchemaGeneratorInterface;
use Mcp\Capability\Registry;
use Mcp\Capability\Registry\Container;
use Mcp\Capability\Registry\ElementReference;
Expand Down Expand Up @@ -58,6 +60,10 @@ final class Builder

private ?ContainerInterface $container = null;

private ?SchemaGeneratorInterface $schemaGenerator = null;

private ?DiscovererInterface $discoverer = null;

private ?SessionFactoryInterface $sessionFactory = null;

private ?SessionStoreInterface $sessionStore = null;
Expand Down Expand Up @@ -286,6 +292,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(),
Expand Down Expand Up @@ -466,11 +486,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) {
Expand Down Expand Up @@ -527,4 +548,15 @@ public function build(): Server

return new Server($protocol, $logger);
}

private function createDiscoverer(LoggerInterface $logger): DiscovererInterface
{
$discoverer = new \Mcp\Capability\Discovery\Discoverer($logger, null, $this->schemaGenerator);

if (null !== $this->discoveryCache) {
return new \Mcp\Capability\Discovery\CachedDiscoverer($discoverer, $this->discoveryCache, $logger);
}

return $discoverer;
}
}
Loading