Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
6 changes: 4 additions & 2 deletions src/Capability/Discovery/CachedDiscoverer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@
* 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,
) {
Expand Down
6 changes: 4 additions & 2 deletions src/Capability/Discovery/Discoverer.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,16 @@
* 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);
Expand Down Expand Up @@ -222,7 +224,7 @@
$name = $instance->name ?? ('__invoke' === $methodName ? $classShortName : $methodName);
$description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null;
$inputSchema = $this->schemaGenerator->generate($method);
$outputSchema = $this->schemaGenerator->generateOutputSchema($method);

Check failure on line 227 in src/Capability/Discovery/Discoverer.php

View workflow job for this annotation

GitHub Actions / qa

Call to an undefined method Mcp\Capability\Discovery\SchemaGeneratorInterface::generateOutputSchema().
$tool = new Tool(
$name,
$inputSchema,
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;
}
17 changes: 13 additions & 4 deletions src/Capability/Discovery/SchemaGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

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;
Expand Down Expand Up @@ -57,20 +58,28 @@
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
class SchemaGenerator
final 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 \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'])) {
Expand Down Expand Up @@ -108,7 +117,7 @@ public function generateOutputSchema(\ReflectionMethod|\ReflectionFunction $refl
*
* @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 @@ -84,13 +85,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
19 changes: 19 additions & 0 deletions src/Exception/BadMethodCallException.php
Original file line number Diff line number Diff line change
@@ -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
{
}
38 changes: 36 additions & 2 deletions src/Server/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -287,6 +295,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 @@ -470,11 +492,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 @@ -531,4 +554,15 @@ public function build(): Server

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