[Server] Allow overriding the default file name pattern for Discovery (#292)
* Add optional namePatterns capability to discovery so that the default scan for `*.php` files can be overridden. * add default to namePatterns docs * add an error check if we manage to pass in an empty list of patterns * add unit tests (and a static entry to classmap for composer.json so it autoloads from a non .php file for the test) * run php-cs-fixer * change how default namePatterns are set and make sure all the annotations are correct * Fix bad whitespace Co-authored-by: Christopher Hertel <mail@christopher-hertel.de> * switch to a const default instead of nullable with a check in the function to convert null to a default * make sure we handle empty arrays * formatting fixes * Apply suggestion from @chr-hertel Co-authored-by: Christopher Hertel <mail@christopher-hertel.de> * Add changelog entry --------- Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
This commit is contained in:
parent
fb2c8c2ee4
commit
86bd845924
10 changed files with 109 additions and 37 deletions
|
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
All notable changes to `mcp/sdk` will be documented in this file.
|
All notable changes to `mcp/sdk` will be documented in this file.
|
||||||
|
|
||||||
|
0.6.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Allow overriding the default name pattern for Discovery
|
||||||
|
|
||||||
0.5.0
|
0.5.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,10 @@
|
||||||
"Mcp\\Example\\Server\\OAuthMicrosoft\\": "examples/server/oauth-microsoft/",
|
"Mcp\\Example\\Server\\OAuthMicrosoft\\": "examples/server/oauth-microsoft/",
|
||||||
"Mcp\\Example\\Server\\SchemaShowcase\\": "examples/server/schema-showcase/",
|
"Mcp\\Example\\Server\\SchemaShowcase\\": "examples/server/schema-showcase/",
|
||||||
"Mcp\\Tests\\": "tests/"
|
"Mcp\\Tests\\": "tests/"
|
||||||
}
|
},
|
||||||
|
"classmap": [
|
||||||
|
"tests/Unit/Capability/Discovery/Fixtures/AlternativeFileNameToolHandler.class.inc"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@ $server = Server::builder()
|
||||||
basePath: __DIR__,
|
basePath: __DIR__,
|
||||||
scanDirs: ['.', 'src', 'lib'], // Where to look for MCP attributes
|
scanDirs: ['.', 'src', 'lib'], // Where to look for MCP attributes
|
||||||
excludeDirs: ['vendor', 'tests'], // Where NOT to look
|
excludeDirs: ['vendor', 'tests'], // Where NOT to look
|
||||||
cache: $cacheInstance // Optional: cache discovered elements
|
cache: $cacheInstance, // Optional: cache discovered elements
|
||||||
|
namePatterns: ['*.php', '*.inc'], // Optional: list of filename patterns to match
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -109,6 +110,7 @@ $server = Server::builder()
|
||||||
- `$scanDirs` (array): Directories to recursively scan for `#[McpTool]`, `#[McpResource]`, etc. All subdirectories are included. (default: `['.', 'src']`)
|
- `$scanDirs` (array): Directories to recursively scan for `#[McpTool]`, `#[McpResource]`, etc. All subdirectories are included. (default: `['.', 'src']`)
|
||||||
- `$excludeDirs` (array): Directory names to exclude **within** the scanned directories during recursive scanning
|
- `$excludeDirs` (array): Directory names to exclude **within** the scanned directories during recursive scanning
|
||||||
- `$cache` (CacheInterface|null): Optional PSR-16 cache to store discovered elements for performance
|
- `$cache` (CacheInterface|null): Optional PSR-16 cache to store discovered elements for performance
|
||||||
|
- `$namePatterns` (array): Optional list of patterns (regexp, glob, or string) for file names (default: `['*.php']`)
|
||||||
|
|
||||||
**Basic Discovery (scans current directory and `src/`):**
|
**Basic Discovery (scans current directory and `src/`):**
|
||||||
```php
|
```php
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,12 @@ final class CachedDiscoverer implements DiscovererInterface
|
||||||
/**
|
/**
|
||||||
* Discover MCP elements in the specified directories with caching.
|
* Discover MCP elements in the specified directories with caching.
|
||||||
*
|
*
|
||||||
* @param string $basePath the base path for resolving directories
|
* @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> $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
|
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
|
||||||
|
* @param array<string> $namePatterns list of file name patterns for the scan. Compatible with Finder->name()
|
||||||
*/
|
*/
|
||||||
public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState
|
public function discover(string $basePath, array $directories, array $excludeDirs = [], array $namePatterns = self::DEFAULT_NAME_PATERNS): DiscoveryState
|
||||||
{
|
{
|
||||||
$cacheKey = $this->generateCacheKey($basePath, $directories, $excludeDirs);
|
$cacheKey = $this->generateCacheKey($basePath, $directories, $excludeDirs);
|
||||||
|
|
||||||
|
|
@ -63,7 +64,7 @@ final class CachedDiscoverer implements DiscovererInterface
|
||||||
'directories' => $directories,
|
'directories' => $directories,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$discoveryState = $this->discoverer->discover($basePath, $directories, $excludeDirs);
|
$discoveryState = $this->discoverer->discover($basePath, $directories, $excludeDirs, $namePatterns);
|
||||||
|
|
||||||
$this->cache->set($cacheKey, $discoveryState);
|
$this->cache->set($cacheKey, $discoveryState);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,12 @@ final class Discoverer implements DiscovererInterface
|
||||||
/**
|
/**
|
||||||
* Discover MCP elements in the specified directories and return the discovery state.
|
* Discover MCP elements in the specified directories and return the discovery state.
|
||||||
*
|
*
|
||||||
* @param string $basePath the base path for resolving directories
|
* @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> $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
|
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
|
||||||
|
* @param array<string> $namePatterns list of file name patterns for the scan. Compatible with Finder->name()
|
||||||
*/
|
*/
|
||||||
public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState
|
public function discover(string $basePath, array $directories, array $excludeDirs = [], array $namePatterns = self::DEFAULT_NAME_PATERNS): DiscoveryState
|
||||||
{
|
{
|
||||||
$startTime = microtime(true);
|
$startTime = microtime(true);
|
||||||
$discoveredCount = [
|
$discoveredCount = [
|
||||||
|
|
@ -79,6 +80,8 @@ final class Discoverer implements DiscovererInterface
|
||||||
'resourceTemplates' => 0,
|
'resourceTemplates' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$namePatterns = !empty($namePatterns) ? $namePatterns : self::DEFAULT_NAME_PATERNS;
|
||||||
|
|
||||||
$tools = [];
|
$tools = [];
|
||||||
$resources = [];
|
$resources = [];
|
||||||
$prompts = [];
|
$prompts = [];
|
||||||
|
|
@ -106,7 +109,7 @@ final class Discoverer implements DiscovererInterface
|
||||||
$finder->files()
|
$finder->files()
|
||||||
->in($absolutePaths)
|
->in($absolutePaths)
|
||||||
->exclude($excludeDirs)
|
->exclude($excludeDirs)
|
||||||
->name('*.php');
|
->name($namePatterns);
|
||||||
|
|
||||||
foreach ($finder as $file) {
|
foreach ($finder as $file) {
|
||||||
$this->processFile($file, $discoveredCount, $tools, $resources, $prompts, $resourceTemplates);
|
$this->processFile($file, $discoveredCount, $tools, $resources, $prompts, $resourceTemplates);
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,15 @@ namespace Mcp\Capability\Discovery;
|
||||||
*/
|
*/
|
||||||
interface DiscovererInterface
|
interface DiscovererInterface
|
||||||
{
|
{
|
||||||
|
public const DEFAULT_NAME_PATERNS = ['*.php'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discover MCP elements in the specified directories and return the discovery state.
|
* Discover MCP elements in the specified directories and return the discovery state.
|
||||||
*
|
*
|
||||||
* @param string $basePath the base path for resolving directories
|
* @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> $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
|
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
|
||||||
|
* @param array<string> $namePatterns list of file name patterns for the scan. Compatible with Finder->name()
|
||||||
*/
|
*/
|
||||||
public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState;
|
public function discover(string $basePath, array $directories, array $excludeDirs = [], array $namePatterns = self::DEFAULT_NAME_PATERNS): DiscoveryState;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,20 @@ final class DiscoveryLoader implements LoaderInterface
|
||||||
/**
|
/**
|
||||||
* @param string[] $scanDirs
|
* @param string[] $scanDirs
|
||||||
* @param array|string[] $excludeDirs
|
* @param array|string[] $excludeDirs
|
||||||
|
* @param string[] $namePatterns
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $basePath,
|
private string $basePath,
|
||||||
private array $scanDirs,
|
private array $scanDirs,
|
||||||
private array $excludeDirs,
|
private array $excludeDirs,
|
||||||
private DiscovererInterface $discoverer,
|
private DiscovererInterface $discoverer,
|
||||||
|
private array $namePatterns = DiscovererInterface::DEFAULT_NAME_PATERNS,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function load(RegistryInterface $registry): void
|
public function load(RegistryInterface $registry): void
|
||||||
{
|
{
|
||||||
$discoveryState = $this->discoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs);
|
$discoveryState = $this->discoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs, $this->namePatterns);
|
||||||
|
|
||||||
$registry->setDiscoveryState($discoveryState);
|
$registry->setDiscoveryState($discoveryState);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,11 @@ final class Builder
|
||||||
*/
|
*/
|
||||||
private array $discoveryExcludeDirs = [];
|
private array $discoveryExcludeDirs = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[]|null
|
||||||
|
*/
|
||||||
|
private ?array $discoveryNamePatterns = null;
|
||||||
|
|
||||||
private ?ServerCapabilities $serverCapabilities = null;
|
private ?ServerCapabilities $serverCapabilities = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -348,17 +353,20 @@ final class Builder
|
||||||
/**
|
/**
|
||||||
* @param string[] $scanDirs
|
* @param string[] $scanDirs
|
||||||
* @param string[] $excludeDirs
|
* @param string[] $excludeDirs
|
||||||
|
* @param string[] $namePatterns
|
||||||
*/
|
*/
|
||||||
public function setDiscovery(
|
public function setDiscovery(
|
||||||
string $basePath,
|
string $basePath,
|
||||||
array $scanDirs = ['.', 'src'],
|
array $scanDirs = ['.', 'src'],
|
||||||
array $excludeDirs = [],
|
array $excludeDirs = [],
|
||||||
?CacheInterface $cache = null,
|
?CacheInterface $cache = null,
|
||||||
|
array $namePatterns = DiscovererInterface::DEFAULT_NAME_PATERNS,
|
||||||
): self {
|
): self {
|
||||||
$this->discoveryBasePath = $basePath;
|
$this->discoveryBasePath = $basePath;
|
||||||
$this->discoveryScanDirs = $scanDirs;
|
$this->discoveryScanDirs = $scanDirs;
|
||||||
$this->discoveryExcludeDirs = $excludeDirs;
|
$this->discoveryExcludeDirs = $excludeDirs;
|
||||||
$this->discoveryCache = $cache;
|
$this->discoveryCache = $cache;
|
||||||
|
$this->discoveryNamePatterns = $namePatterns;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
@ -531,7 +539,7 @@ final class Builder
|
||||||
if (null !== $this->discoveryBasePath) {
|
if (null !== $this->discoveryBasePath) {
|
||||||
if (null !== $this->discoverer || class_exists(Finder::class)) {
|
if (null !== $this->discoverer || class_exists(Finder::class)) {
|
||||||
$discoverer = $this->discoverer ?? $this->createDiscoverer($logger);
|
$discoverer = $this->discoverer ?? $this->createDiscoverer($logger);
|
||||||
$loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $discoverer);
|
$loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $discoverer, $this->discoveryNamePatterns);
|
||||||
} else {
|
} else {
|
||||||
$logger->warning('File-based discovery requires symfony/finder. Skipping automatic discovery. Run: composer require symfony/finder');
|
$logger->warning('File-based discovery requires symfony/finder. Skipping automatic discovery. Run: composer require symfony/finder');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use Mcp\Capability\Completion\ListCompletionProvider;
|
||||||
use Mcp\Capability\Discovery\Discoverer;
|
use Mcp\Capability\Discovery\Discoverer;
|
||||||
use Mcp\Capability\Registry\ToolReference;
|
use Mcp\Capability\Registry\ToolReference;
|
||||||
use Mcp\Tests\Unit\Capability\Attribute\CompletionProviderFixture;
|
use Mcp\Tests\Unit\Capability\Attribute\CompletionProviderFixture;
|
||||||
|
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\AlternativeFileNameToolHandler;
|
||||||
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\DiscoverableToolHandler;
|
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\DiscoverableToolHandler;
|
||||||
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\InvocablePromptFixture;
|
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\InvocablePromptFixture;
|
||||||
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\InvocableResourceFixture;
|
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\InvocableResourceFixture;
|
||||||
|
|
@ -34,10 +35,10 @@ class DiscoveryTest extends TestCase
|
||||||
|
|
||||||
public function testDiscoversAllElementTypesCorrectlyFromFixtureFiles(): void
|
public function testDiscoversAllElementTypesCorrectlyFromFixtureFiles(): void
|
||||||
{
|
{
|
||||||
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures']);
|
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], ['*.php', '*.inc']);
|
||||||
|
|
||||||
$tools = $discovery->getTools();
|
$tools = $discovery->getTools();
|
||||||
$this->assertCount(4, $tools);
|
$this->assertCount(5, $tools);
|
||||||
|
|
||||||
$this->assertArrayHasKey('greet_user', $tools);
|
$this->assertArrayHasKey('greet_user', $tools);
|
||||||
$this->assertFalse($tools['greet_user']->isManual);
|
$this->assertFalse($tools['greet_user']->isManual);
|
||||||
|
|
@ -56,6 +57,9 @@ class DiscoveryTest extends TestCase
|
||||||
$this->assertFalse($tools['InvokableCalculator']->isManual);
|
$this->assertFalse($tools['InvokableCalculator']->isManual);
|
||||||
$this->assertEquals([InvocableToolFixture::class, '__invoke'], $tools['InvokableCalculator']->handler);
|
$this->assertEquals([InvocableToolFixture::class, '__invoke'], $tools['InvokableCalculator']->handler);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('inc_file_name_tool', $tools);
|
||||||
|
$this->assertEquals([AlternativeFileNameToolHandler::class, 'run'], $tools['inc_file_name_tool']->handler);
|
||||||
|
|
||||||
$this->assertArrayNotHasKey('private_tool_should_be_ignored', $tools);
|
$this->assertArrayNotHasKey('private_tool_should_be_ignored', $tools);
|
||||||
$this->assertArrayNotHasKey('protected_tool_should_be_ignored', $tools);
|
$this->assertArrayNotHasKey('protected_tool_should_be_ignored', $tools);
|
||||||
$this->assertArrayNotHasKey('static_tool_should_be_ignored', $tools);
|
$this->assertArrayNotHasKey('static_tool_should_be_ignored', $tools);
|
||||||
|
|
@ -121,6 +125,25 @@ class DiscoveryTest extends TestCase
|
||||||
$this->assertTrue($discovery->isEmpty());
|
$this->assertTrue($discovery->isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testHandlesDefaultAndOverriddenFileNamePatterns(): void
|
||||||
|
{
|
||||||
|
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures']);
|
||||||
|
$this->assertArrayHasKey('greet_user', $discovery->getTools());
|
||||||
|
$this->assertArrayNotHasKey('inc_file_name_tool', $discovery->getTools());
|
||||||
|
|
||||||
|
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], []);
|
||||||
|
$this->assertArrayHasKey('greet_user', $discovery->getTools());
|
||||||
|
$this->assertArrayNotHasKey('inc_file_name_tool', $discovery->getTools());
|
||||||
|
|
||||||
|
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], ['*.php', '*.inc']);
|
||||||
|
$this->assertArrayHasKey('greet_user', $discovery->getTools());
|
||||||
|
$this->assertArrayHasKey('inc_file_name_tool', $discovery->getTools());
|
||||||
|
|
||||||
|
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], ['*.inc']);
|
||||||
|
$this->assertArrayNotHasKey('greet_user', $discovery->getTools());
|
||||||
|
$this->assertArrayHasKey('inc_file_name_tool', $discovery->getTools());
|
||||||
|
}
|
||||||
|
|
||||||
public function testCorrectlyInfersNamesAndDescriptionsFromMethodsOrClassesIfNotSetInAttribute(): void
|
public function testCorrectlyInfersNamesAndDescriptionsFromMethodsOrClassesIfNotSetInAttribute(): void
|
||||||
{
|
{
|
||||||
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures']);
|
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures']);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?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\Tests\Unit\Capability\Discovery\Fixtures;
|
||||||
|
|
||||||
|
use Mcp\Capability\Attribute\McpTool;
|
||||||
|
|
||||||
|
class AlternativeFileNameToolHandler
|
||||||
|
{
|
||||||
|
#[McpTool(name: 'inc_file_name_tool')]
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue