Skip to content

Commit 16a7df0

Browse files
committed
Merge branch 'main' into allow-runtime-tools
2 parents 1193528 + 5e0731f commit 16a7df0

36 files changed

Lines changed: 408 additions & 84 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
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+
* Add configurable session garbage collection (`gcProbability`/`gcDivisor`)
10+
511
0.5.0
612
-----
713

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: 59 additions & 23 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
@@ -154,11 +156,6 @@ use Mcp\Server\Session\Psr16SessionStore;
154156
use Symfony\Component\Cache\Psr16Cache;
155157
use Symfony\Component\Cache\Adapter\RedisAdapter;
156158

157-
// Use default in-memory sessions with custom TTL
158-
$server = Server::builder()
159-
->setSession(ttl: 7200) // 2 hours
160-
->build();
161-
162159
// Override with file-based storage
163160
$server = Server::builder()
164161
->setSession(new FileSessionStore(__DIR__ . '/sessions'))
@@ -186,6 +183,45 @@ $server = Server::builder()
186183
->build();
187184
```
188185

186+
### Garbage Collection Configuration
187+
188+
The SDK periodically runs garbage collection to clean up expired sessions, similar to PHP's native
189+
`session.gc_probability` and `session.gc_divisor` settings. The probability that GC runs on any given
190+
request is `gcProbability / gcDivisor`.
191+
192+
```php
193+
// Default: 1/100 (1% chance per request)
194+
$server = Server::builder()
195+
->setSession(new FileSessionStore(__DIR__ . '/sessions'))
196+
->build();
197+
198+
// Higher frequency: 1/10 (10% chance per request)
199+
$server = Server::builder()
200+
->setSession(
201+
new FileSessionStore(__DIR__ . '/sessions'),
202+
gcProbability: 1,
203+
gcDivisor: 10,
204+
)
205+
->build();
206+
207+
// Run GC on every request
208+
$server = Server::builder()
209+
->setSession(gcProbability: 1, gcDivisor: 1)
210+
->build();
211+
212+
// Disable GC entirely (e.g. when using an external cleanup process)
213+
$server = Server::builder()
214+
->setSession(gcProbability: 0)
215+
->build();
216+
```
217+
218+
**Parameters:**
219+
- `$gcProbability` (int): The numerator of the GC probability fraction (default: `1`). Set to `0` to disable GC.
220+
- `$gcDivisor` (int): The denominator of the GC probability fraction (default: `100`). Must be >= 1.
221+
222+
> **Note**: When providing a custom `SessionManagerInterface` via the `$sessionManager` parameter,
223+
> the `gcProbability` and `gcDivisor` settings are ignored — you control GC behavior in your own implementation.
224+
189225
**Available Session Stores:**
190226
- `InMemorySessionStore`: Fast in-memory storage (default)
191227
- `FileSessionStore`: Persistent file-based storage
@@ -255,19 +291,19 @@ $server = Server::builder()
255291
name: 'add_numbers',
256292
description: 'Adds two numbers together'
257293
)
258-
294+
259295
// Using class method pair
260296
->addTool(
261297
handler: [Calculator::class, 'multiply'],
262298
name: 'multiply_numbers'
263299
// name and description are optional - derived from method name and docblock
264300
)
265-
301+
266302
// Using instance method
267303
->addTool(
268304
handler: [$calculatorInstance, 'divide']
269305
)
270-
306+
271307
// Using invokable class
272308
->addTool(
273309
handler: InvokableCalculator::class
@@ -482,17 +518,17 @@ $server = Server::builder()
482518
individual JSON-RPC messages. They do not receive the builder's registry, container, or discovery output unless you pass
483519
those dependencies in yourself.
484520

485-
> **Warning**: Custom message handlers bypass discovery, manual capability registration, and container lookups (unless
521+
> **Warning**: Custom message handlers bypass discovery, manual capability registration, and container lookups (unless
486522
> you explicitly pass them). Tools, resources, and prompts you register elsewhere will not show up unless your handler
487-
> loads and executes them manually. Reach for this API only when you need that level of control and are comfortable
523+
> loads and executes them manually. Reach for this API only when you need that level of control and are comfortable
488524
> taking on the additional plumbing.
489525
490526
### Request Handlers
491527

492-
Handle JSON-RPC requests (messages with an `id` that expect a response). Request handlers **must** return either a
528+
Handle JSON-RPC requests (messages with an `id` that expect a response). Request handlers **must** return either a
493529
`Response` or an `Error` object.
494530

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

498534
```php
@@ -569,7 +605,7 @@ interface NotificationHandlerInterface
569605

570606
### Example
571607

572-
Check out `examples/custom-method-handlers/server.php` for a complete example showing how to implement
608+
Check out `examples/custom-method-handlers/server.php` for a complete example showing how to implement
573609
custom `tools/list` and `tools/call` request handlers independently of the registry.
574610

575611
## Complete Example
@@ -601,25 +637,25 @@ $container->set(DatabaseService::class, new DatabaseService($container->get(\PDO
601637
$server = Server::builder()
602638
// Server identity
603639
->setServerInfo('Advanced Calculator', '2.1.0')
604-
640+
605641
// Performance and behavior
606642
->setPaginationLimit(100)
607643
->setInstructions('Use calculate tool for math operations. Check config resource for current settings.')
608-
644+
609645
// Discovery with caching
610646
->setDiscovery(__DIR__, ['src'], ['vendor', 'tests'], $cache)
611-
647+
612648
// Session management
613649
->setSession($sessionStore)
614-
650+
615651
// Services
616652
->setLogger($logger)
617653
->setContainer($container)
618-
654+
619655
// Manual capability registration
620656
->addTool([Calculator::class, 'advancedCalculation'], 'advanced_calc')
621657
->addResource([Config::class, 'getSettings'], 'config://app/settings', 'app_settings')
622-
658+
623659
// Build the server
624660
->build();
625661
```
@@ -632,7 +668,7 @@ $server = Server::builder()
632668
| `setPaginationLimit()` | limit | Set max items per page |
633669
| `setInstructions()` | instructions | Set usage instructions |
634670
| `setDiscovery()` | basePath, scanDirs?, excludeDirs?, cache? | Configure attribute discovery |
635-
| `setSession()` | store?, factory?, ttl? | Configure session management |
671+
| `setSession()` | sessionStore?, sessionManager?, gcProbability?, gcDivisor? | Configure session management |
636672
| `setLogger()` | logger | Set PSR-3 logger |
637673
| `setContainer()` | container | Set PSR-11 container |
638674
| `setEventDispatcher()` | dispatcher | Set PSR-14 event dispatcher |

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: 16 additions & 9 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,15 +109,14 @@ 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);
113116
}
114117
} catch (\Throwable $e) {
115118
$this->logger->error('Error during file finding process for MCP discovery'.json_encode($e->getTrace(), \JSON_PRETTY_PRINT), [
116119
'exception' => $e,
117-
'trace' => $e->getTraceAsString(),
118120
]);
119121
}
120122

@@ -196,7 +198,6 @@ private function processFile(SplFileInfo $file, array &$discoveredCount, array &
196198
'file' => $file->getPathname(),
197199
'class' => $className,
198200
'exception' => $e,
199-
'trace' => $e->getTraceAsString(),
200201
]);
201202
}
202203
}
@@ -297,9 +298,15 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun
297298
break;
298299
}
299300
} catch (ExceptionInterface $e) {
300-
$this->logger->error("Failed to process MCP attribute on {$className}::{$methodName}", ['attribute' => $attributeClassName, 'exception' => $e, 'trace' => $e->getPrevious() ? $e->getPrevious()->getTraceAsString() : $e->getTraceAsString()]);
301+
$this->logger->error("Failed to process MCP attribute on {$className}::{$methodName}", [
302+
'attribute' => $attributeClassName,
303+
'exception' => $e,
304+
]);
301305
} catch (\Throwable $e) {
302-
$this->logger->error("Unexpected error processing attribute on {$className}::{$methodName}", ['attribute' => $attributeClassName, 'exception' => $e, 'trace' => $e->getTraceAsString()]);
306+
$this->logger->error("Unexpected error processing attribute on {$className}::{$methodName}", [
307+
'attribute' => $attributeClassName,
308+
'exception' => $e,
309+
]);
303310
}
304311
}
305312

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/Discovery/DocBlockParser.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ public function parseDocBlock(string|false|null $docComment): ?DocBlock
4747
} catch (\Throwable $e) {
4848
// Log error or handle gracefully if invalid DocBlock syntax is encountered
4949
$this->logger->warning('Failed to parse DocBlock', [
50-
'error' => $e->getMessage(),
51-
'exception_trace' => $e->getTraceAsString(),
50+
'exception' => $e,
5251
]);
5352

5453
return null;

src/Capability/Discovery/SchemaGenerator.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,8 @@ private function applyEnumConstraints(array $paramSchema, array $paramInfo): arr
374374

375375
if ($jsonBackingType) {
376376
if (isset($paramSchema['type']) && \is_array($paramSchema['type']) && \in_array('null', $paramSchema['type'])) {
377-
$paramSchema['type'] = ['null', $jsonBackingType];
377+
$paramSchema['type'] = [$jsonBackingType, 'null'];
378+
$paramSchema['enum'][] = null;
378379
} else {
379380
$paramSchema['type'] = $jsonBackingType;
380381
}
@@ -383,7 +384,8 @@ private function applyEnumConstraints(array $paramSchema, array $paramInfo): arr
383384
// Non-backed enum - use names as enum values
384385
$paramSchema['enum'] = array_column($enumClass::cases(), 'name');
385386
if (isset($paramSchema['type']) && \is_array($paramSchema['type']) && \in_array('null', $paramSchema['type'])) {
386-
$paramSchema['type'] = ['null', 'string'];
387+
$paramSchema['type'] = ['string', 'null'];
388+
$paramSchema['enum'][] = null;
387389
} else {
388390
$paramSchema['type'] = 'string';
389391
}

src/Capability/Discovery/SchemaValidator.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ public function validateAgainstJsonSchema(mixed $data, array|object $schema): ar
8787
$result = $validator->validate($dataToValidate, $schemaObject);
8888
} catch (\Throwable $e) {
8989
$this->logger->error('MCP SDK: JSON Schema validation failed internally.', [
90-
'exception_message' => $e->getMessage(),
91-
'exception_trace' => $e->getTraceAsString(),
90+
'exception' => $e,
9291
'data' => json_encode($dataToValidate),
9392
'schema' => json_encode($schemaObject),
9493
]);

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
}

0 commit comments

Comments
 (0)