Skip to content

Commit 86bd845

Browse files
[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>
1 parent fb2c8c2 commit 86bd845

10 files changed

Lines changed: 109 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes to `mcp/sdk` will be documented in this file.
44

5+
0.6.0
6+
-----
7+
8+
* Allow overriding the default name pattern for Discovery
9+
510
0.5.0
611
-----
712

composer.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@
8080
"Mcp\\Example\\Server\\OAuthMicrosoft\\": "examples/server/oauth-microsoft/",
8181
"Mcp\\Example\\Server\\SchemaShowcase\\": "examples/server/schema-showcase/",
8282
"Mcp\\Tests\\": "tests/"
83-
}
83+
},
84+
"classmap": [
85+
"tests/Unit/Capability/Discovery/Fixtures/AlternativeFileNameToolHandler.class.inc"
86+
]
8487
},
8588
"config": {
8689
"allow-plugins": {
@@ -89,4 +92,4 @@
8992
},
9093
"sort-packages": true
9194
}
92-
}
95+
}

docs/server-builder.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ $server = Server::builder()
9999
->setDiscovery(
100100
basePath: __DIR__,
101101
scanDirs: ['.', 'src', 'lib'], // Where to look for MCP attributes
102-
excludeDirs: ['vendor', 'tests'], // Where NOT to look
103-
cache: $cacheInstance // Optional: cache discovered elements
102+
excludeDirs: ['vendor', 'tests'], // Where NOT to look
103+
cache: $cacheInstance, // Optional: cache discovered elements
104+
namePatterns: ['*.php', '*.inc'], // Optional: list of filename patterns to match
104105
);
105106
```
106107

@@ -109,6 +110,7 @@ $server = Server::builder()
109110
- `$scanDirs` (array): Directories to recursively scan for `#[McpTool]`, `#[McpResource]`, etc. All subdirectories are included. (default: `['.', 'src']`)
110111
- `$excludeDirs` (array): Directory names to exclude **within** the scanned directories during recursive scanning
111112
- `$cache` (CacheInterface|null): Optional PSR-16 cache to store discovered elements for performance
113+
- `$namePatterns` (array): Optional list of patterns (regexp, glob, or string) for file names (default: `['*.php']`)
112114

113115
**Basic Discovery (scans current directory and `src/`):**
114116
```php
@@ -137,7 +139,7 @@ $server = Server::builder()
137139

138140
**How `excludeDirs` works:**
139141
- If scanning `src/` and there's `src/vendor/`, it will be excluded
140-
- If scanning `lib/` and there's `lib/tests/`, it will be excluded
142+
- If scanning `lib/` and there's `lib/tests/`, it will be excluded
141143
- But if `vendor/` and `tests/` are at the same level as `src/`, they're not scanned anyway (not in `scanDirs`)
142144

143145
> **Performance**: Always use a cache in production. The first run scans and caches all discovered MCP elements, making
@@ -255,19 +257,19 @@ $server = Server::builder()
255257
name: 'add_numbers',
256258
description: 'Adds two numbers together'
257259
)
258-
260+
259261
// Using class method pair
260262
->addTool(
261263
handler: [Calculator::class, 'multiply'],
262264
name: 'multiply_numbers'
263265
// name and description are optional - derived from method name and docblock
264266
)
265-
267+
266268
// Using instance method
267269
->addTool(
268270
handler: [$calculatorInstance, 'divide']
269271
)
270-
272+
271273
// Using invokable class
272274
->addTool(
273275
handler: InvokableCalculator::class
@@ -421,17 +423,17 @@ $server = Server::builder()
421423
individual JSON-RPC messages. They do not receive the builder's registry, container, or discovery output unless you pass
422424
those dependencies in yourself.
423425

424-
> **Warning**: Custom message handlers bypass discovery, manual capability registration, and container lookups (unless
426+
> **Warning**: Custom message handlers bypass discovery, manual capability registration, and container lookups (unless
425427
> you explicitly pass them). Tools, resources, and prompts you register elsewhere will not show up unless your handler
426-
> loads and executes them manually. Reach for this API only when you need that level of control and are comfortable
428+
> loads and executes them manually. Reach for this API only when you need that level of control and are comfortable
427429
> taking on the additional plumbing.
428430
429431
### Request Handlers
430432

431-
Handle JSON-RPC requests (messages with an `id` that expect a response). Request handlers **must** return either a
433+
Handle JSON-RPC requests (messages with an `id` that expect a response). Request handlers **must** return either a
432434
`Response` or an `Error` object.
433435

434-
Attach request handlers with `addRequestHandler()` (single) or `addRequestHandlers()` (multiple). You can call these
436+
Attach request handlers with `addRequestHandler()` (single) or `addRequestHandlers()` (multiple). You can call these
435437
methods as many times as needed; each call prepends the handlers so they execute before the defaults:
436438

437439
```php
@@ -508,7 +510,7 @@ interface NotificationHandlerInterface
508510

509511
### Example
510512

511-
Check out `examples/custom-method-handlers/server.php` for a complete example showing how to implement
513+
Check out `examples/custom-method-handlers/server.php` for a complete example showing how to implement
512514
custom `tools/list` and `tools/call` request handlers independently of the registry.
513515

514516
## Complete Example
@@ -540,25 +542,25 @@ $container->set(DatabaseService::class, new DatabaseService($container->get(\PDO
540542
$server = Server::builder()
541543
// Server identity
542544
->setServerInfo('Advanced Calculator', '2.1.0')
543-
545+
544546
// Performance and behavior
545547
->setPaginationLimit(100)
546548
->setInstructions('Use calculate tool for math operations. Check config resource for current settings.')
547-
549+
548550
// Discovery with caching
549551
->setDiscovery(__DIR__, ['src'], ['vendor', 'tests'], $cache)
550-
552+
551553
// Session management
552554
->setSession($sessionStore)
553-
555+
554556
// Services
555557
->setLogger($logger)
556558
->setContainer($container)
557-
559+
558560
// Manual capability registration
559561
->addTool([Calculator::class, 'advancedCalculation'], 'advanced_calc')
560562
->addResource([Config::class, 'getSettings'], 'config://app/settings', 'app_settings')
561-
563+
562564
// Build the server
563565
->build();
564566
```

src/Capability/Discovery/CachedDiscoverer.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@ public function __construct(
3838
/**
3939
* Discover MCP elements in the specified directories with caching.
4040
*
41-
* @param string $basePath the base path for resolving directories
42-
* @param array<string> $directories list of directories (relative to base path) to scan
43-
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
41+
* @param string $basePath the base path for resolving directories
42+
* @param array<string> $directories list of directories (relative to base path) to scan
43+
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
44+
* @param array<string> $namePatterns list of file name patterns for the scan. Compatible with Finder->name()
4445
*/
45-
public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState
46+
public function discover(string $basePath, array $directories, array $excludeDirs = [], array $namePatterns = self::DEFAULT_NAME_PATERNS): DiscoveryState
4647
{
4748
$cacheKey = $this->generateCacheKey($basePath, $directories, $excludeDirs);
4849

@@ -63,7 +64,7 @@ public function discover(string $basePath, array $directories, array $excludeDir
6364
'directories' => $directories,
6465
]);
6566

66-
$discoveryState = $this->discoverer->discover($basePath, $directories, $excludeDirs);
67+
$discoveryState = $this->discoverer->discover($basePath, $directories, $excludeDirs, $namePatterns);
6768

6869
$this->cache->set($cacheKey, $discoveryState);
6970

src/Capability/Discovery/Discoverer.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,12 @@ public function __construct(
6565
/**
6666
* Discover MCP elements in the specified directories and return the discovery state.
6767
*
68-
* @param string $basePath the base path for resolving directories
69-
* @param array<string> $directories list of directories (relative to base path) to scan
70-
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
68+
* @param string $basePath the base path for resolving directories
69+
* @param array<string> $directories list of directories (relative to base path) to scan
70+
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
71+
* @param array<string> $namePatterns list of file name patterns for the scan. Compatible with Finder->name()
7172
*/
72-
public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState
73+
public function discover(string $basePath, array $directories, array $excludeDirs = [], array $namePatterns = self::DEFAULT_NAME_PATERNS): DiscoveryState
7374
{
7475
$startTime = microtime(true);
7576
$discoveredCount = [
@@ -79,6 +80,8 @@ public function discover(string $basePath, array $directories, array $excludeDir
7980
'resourceTemplates' => 0,
8081
];
8182

83+
$namePatterns = !empty($namePatterns) ? $namePatterns : self::DEFAULT_NAME_PATERNS;
84+
8285
$tools = [];
8386
$resources = [];
8487
$prompts = [];
@@ -106,7 +109,7 @@ public function discover(string $basePath, array $directories, array $excludeDir
106109
$finder->files()
107110
->in($absolutePaths)
108111
->exclude($excludeDirs)
109-
->name('*.php');
112+
->name($namePatterns);
110113

111114
foreach ($finder as $file) {
112115
$this->processFile($file, $discoveredCount, $tools, $resources, $prompts, $resourceTemplates);

src/Capability/Discovery/DiscovererInterface.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
*/
2121
interface DiscovererInterface
2222
{
23+
public const DEFAULT_NAME_PATERNS = ['*.php'];
24+
2325
/**
2426
* Discover MCP elements in the specified directories and return the discovery state.
2527
*
26-
* @param string $basePath the base path for resolving directories
27-
* @param array<string> $directories list of directories (relative to base path) to scan
28-
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
28+
* @param string $basePath the base path for resolving directories
29+
* @param array<string> $directories list of directories (relative to base path) to scan
30+
* @param array<string> $excludeDirs list of directories (relative to base path) to exclude from the scan
31+
* @param array<string> $namePatterns list of file name patterns for the scan. Compatible with Finder->name()
2932
*/
30-
public function discover(string $basePath, array $directories, array $excludeDirs = []): DiscoveryState;
33+
public function discover(string $basePath, array $directories, array $excludeDirs = [], array $namePatterns = self::DEFAULT_NAME_PATERNS): DiscoveryState;
3134
}

src/Capability/Registry/Loader/DiscoveryLoader.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,20 @@ final class DiscoveryLoader implements LoaderInterface
2424
/**
2525
* @param string[] $scanDirs
2626
* @param array|string[] $excludeDirs
27+
* @param string[] $namePatterns
2728
*/
2829
public function __construct(
2930
private string $basePath,
3031
private array $scanDirs,
3132
private array $excludeDirs,
3233
private DiscovererInterface $discoverer,
34+
private array $namePatterns = DiscovererInterface::DEFAULT_NAME_PATERNS,
3335
) {
3436
}
3537

3638
public function load(RegistryInterface $registry): void
3739
{
38-
$discoveryState = $this->discoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs);
40+
$discoveryState = $this->discoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs, $this->namePatterns);
3941

4042
$registry->setDiscoveryState($discoveryState);
4143
}

src/Server/Builder.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ final class Builder
160160
*/
161161
private array $discoveryExcludeDirs = [];
162162

163+
/**
164+
* @var string[]|null
165+
*/
166+
private ?array $discoveryNamePatterns = null;
167+
163168
private ?ServerCapabilities $serverCapabilities = null;
164169

165170
/**
@@ -348,17 +353,20 @@ public function setSession(
348353
/**
349354
* @param string[] $scanDirs
350355
* @param string[] $excludeDirs
356+
* @param string[] $namePatterns
351357
*/
352358
public function setDiscovery(
353359
string $basePath,
354360
array $scanDirs = ['.', 'src'],
355361
array $excludeDirs = [],
356362
?CacheInterface $cache = null,
363+
array $namePatterns = DiscovererInterface::DEFAULT_NAME_PATERNS,
357364
): self {
358365
$this->discoveryBasePath = $basePath;
359366
$this->discoveryScanDirs = $scanDirs;
360367
$this->discoveryExcludeDirs = $excludeDirs;
361368
$this->discoveryCache = $cache;
369+
$this->discoveryNamePatterns = $namePatterns;
362370

363371
return $this;
364372
}
@@ -531,7 +539,7 @@ public function build(): Server
531539
if (null !== $this->discoveryBasePath) {
532540
if (null !== $this->discoverer || class_exists(Finder::class)) {
533541
$discoverer = $this->discoverer ?? $this->createDiscoverer($logger);
534-
$loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $discoverer);
542+
$loaders[] = new DiscoveryLoader($this->discoveryBasePath, $this->discoveryScanDirs, $this->discoveryExcludeDirs, $discoverer, $this->discoveryNamePatterns);
535543
} else {
536544
$logger->warning('File-based discovery requires symfony/finder. Skipping automatic discovery. Run: composer require symfony/finder');
537545
}

tests/Unit/Capability/Discovery/DiscoveryTest.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Mcp\Capability\Discovery\Discoverer;
1717
use Mcp\Capability\Registry\ToolReference;
1818
use Mcp\Tests\Unit\Capability\Attribute\CompletionProviderFixture;
19+
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\AlternativeFileNameToolHandler;
1920
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\DiscoverableToolHandler;
2021
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\InvocablePromptFixture;
2122
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\InvocableResourceFixture;
@@ -34,10 +35,10 @@ protected function setUp(): void
3435

3536
public function testDiscoversAllElementTypesCorrectlyFromFixtureFiles(): void
3637
{
37-
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures']);
38+
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], ['*.php', '*.inc']);
3839

3940
$tools = $discovery->getTools();
40-
$this->assertCount(4, $tools);
41+
$this->assertCount(5, $tools);
4142

4243
$this->assertArrayHasKey('greet_user', $tools);
4344
$this->assertFalse($tools['greet_user']->isManual);
@@ -56,6 +57,9 @@ public function testDiscoversAllElementTypesCorrectlyFromFixtureFiles(): void
5657
$this->assertFalse($tools['InvokableCalculator']->isManual);
5758
$this->assertEquals([InvocableToolFixture::class, '__invoke'], $tools['InvokableCalculator']->handler);
5859

60+
$this->assertArrayHasKey('inc_file_name_tool', $tools);
61+
$this->assertEquals([AlternativeFileNameToolHandler::class, 'run'], $tools['inc_file_name_tool']->handler);
62+
5963
$this->assertArrayNotHasKey('private_tool_should_be_ignored', $tools);
6064
$this->assertArrayNotHasKey('protected_tool_should_be_ignored', $tools);
6165
$this->assertArrayNotHasKey('static_tool_should_be_ignored', $tools);
@@ -121,6 +125,25 @@ public function testHandlesEmptyDirectoriesOrDirectoriesWithNoPhpFiles(): void
121125
$this->assertTrue($discovery->isEmpty());
122126
}
123127

128+
public function testHandlesDefaultAndOverriddenFileNamePatterns(): void
129+
{
130+
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures']);
131+
$this->assertArrayHasKey('greet_user', $discovery->getTools());
132+
$this->assertArrayNotHasKey('inc_file_name_tool', $discovery->getTools());
133+
134+
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], []);
135+
$this->assertArrayHasKey('greet_user', $discovery->getTools());
136+
$this->assertArrayNotHasKey('inc_file_name_tool', $discovery->getTools());
137+
138+
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], ['*.php', '*.inc']);
139+
$this->assertArrayHasKey('greet_user', $discovery->getTools());
140+
$this->assertArrayHasKey('inc_file_name_tool', $discovery->getTools());
141+
142+
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], ['*.inc']);
143+
$this->assertArrayNotHasKey('greet_user', $discovery->getTools());
144+
$this->assertArrayHasKey('inc_file_name_tool', $discovery->getTools());
145+
}
146+
124147
public function testCorrectlyInfersNamesAndDescriptionsFromMethodsOrClassesIfNotSetInAttribute(): void
125148
{
126149
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures']);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Tests\Unit\Capability\Discovery\Fixtures;
13+
14+
use Mcp\Capability\Attribute\McpTool;
15+
16+
class AlternativeFileNameToolHandler
17+
{
18+
#[McpTool(name: 'inc_file_name_tool')]
19+
public function run(): void
20+
{
21+
}
22+
}

0 commit comments

Comments
 (0)