[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:
johnhunt-lc 2026-05-08 03:48:29 -04:00 • committed by GitHub
commit 86bd845924
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 109 additions and 37 deletions

View file

@ -38,11 +38,12 @@ final class CachedDiscoverer implements DiscovererInterface
/**
* Discover MCP elements in the specified directories with caching.
*
* @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
* @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
* @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);
@ -63,7 +64,7 @@ final class CachedDiscoverer implements DiscovererInterface
'directories' => $directories,
]);
$discoveryState = $this->discoverer->discover($basePath, $directories, $excludeDirs);
$discoveryState = $this->discoverer->discover($basePath, $directories, $excludeDirs, $namePatterns);
$this->cache->set($cacheKey, $discoveryState);

View file

@ -65,11 +65,12 @@ final class Discoverer implements 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
* @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
* @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);
$discoveredCount = [
@ -79,6 +80,8 @@ final class Discoverer implements DiscovererInterface
'resourceTemplates' => 0,
];
$namePatterns = !empty($namePatterns) ? $namePatterns : self::DEFAULT_NAME_PATERNS;
$tools = [];
$resources = [];
$prompts = [];
@ -106,7 +109,7 @@ final class Discoverer implements DiscovererInterface
$finder->files()
->in($absolutePaths)
->exclude($excludeDirs)
->name('*.php');
->name($namePatterns);
foreach ($finder as $file) {
$this->processFile($file, $discoveredCount, $tools, $resources, $prompts, $resourceTemplates);

View file

@ -20,12 +20,15 @@ namespace Mcp\Capability\Discovery;
*/
interface DiscovererInterface
{
public const DEFAULT_NAME_PATERNS = ['*.php'];
/**
* 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
* @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
* @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;
}

View file

@ -24,18 +24,20 @@ final class DiscoveryLoader implements LoaderInterface
/**
* @param string[] $scanDirs
* @param array|string[] $excludeDirs
* @param string[] $namePatterns
*/
public function __construct(
private string $basePath,
private array $scanDirs,
private array $excludeDirs,
private DiscovererInterface $discoverer,
private array $namePatterns = DiscovererInterface::DEFAULT_NAME_PATERNS,
) {
}
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);
}

View file

@ -160,6 +160,11 @@ final class Builder
*/
private array $discoveryExcludeDirs = [];
/**
* @var string[]|null
*/
private ?array $discoveryNamePatterns = null;
private ?ServerCapabilities $serverCapabilities = null;
/**
@ -348,17 +353,20 @@ final class Builder
/**
* @param string[] $scanDirs
* @param string[] $excludeDirs
* @param string[] $namePatterns
*/
public function setDiscovery(
string $basePath,
array $scanDirs = ['.', 'src'],
array $excludeDirs = [],
?CacheInterface $cache = null,
array $namePatterns = DiscovererInterface::DEFAULT_NAME_PATERNS,
): self {
$this->discoveryBasePath = $basePath;
$this->discoveryScanDirs = $scanDirs;
$this->discoveryExcludeDirs = $excludeDirs;
$this->discoveryCache = $cache;
$this->discoveryNamePatterns = $namePatterns;
return $this;
}
@ -531,7 +539,7 @@ final class Builder
if (null !== $this->discoveryBasePath) {
if (null !== $this->discoverer || class_exists(Finder::class)) {
$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 {
$logger->warning('File-based discovery requires symfony/finder. Skipping automatic discovery. Run: composer require symfony/finder');
}