Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, We will merge this PR in 0.3.0 anyways.

*
* @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
19 changes: 5 additions & 14 deletions src/Capability/Registry/Loader/DiscoveryLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
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;
}
}