Skip to content

Commit beafd6d

Browse files
committed
Add tool name validation for SEP-986
1 parent 85549cb commit beafd6d

File tree

6 files changed

+96
-9
lines changed

6 files changed

+96
-9
lines changed

phpstan.dist.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ parameters:
1515
ignoreErrors:
1616
-
1717
message: "#^Method .*::test.*\\(\\) has no return type specified\\.$#"
18+
-
19+
identifier: missingType.iterableValue
20+
path: tests/
21+
1822
# These errors should actually be fixed, but are ignored for now
1923
-
2024
identifier: missingType.iterableValue

src/Capability/Registry.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Mcp\Capability\Registry\ResourceReference;
1919
use Mcp\Capability\Registry\ResourceTemplateReference;
2020
use Mcp\Capability\Registry\ToolReference;
21+
use Mcp\Capability\Tool\NameValidator;
2122
use Mcp\Event\PromptListChangedEvent;
2223
use Mcp\Event\ResourceListChangedEvent;
2324
use Mcp\Event\ResourceTemplateListChangedEvent;
@@ -71,6 +72,7 @@ final class Registry implements ReferenceProviderInterface, ReferenceRegistryInt
7172
public function __construct(
7273
private readonly ?EventDispatcherInterface $eventDispatcher = null,
7374
private readonly LoggerInterface $logger = new NullLogger(),
75+
private readonly NameValidator $nameValidator = new NameValidator(),
7476
) {
7577
}
7678

@@ -100,12 +102,18 @@ public function registerTool(Tool $tool, callable|array|string $handler, bool $i
100102

101103
if ($existing && !$isManual && $existing->isManual) {
102104
$this->logger->debug(
103-
"Ignoring discovered tool '{$toolName}' as it conflicts with a manually registered one.",
105+
\sprintf('Ignoring discovered tool "%s" as it conflicts with a manually registered one.', $toolName),
104106
);
105107

106108
return;
107109
}
108110

111+
if (!$this->nameValidator->isValid($toolName)) {
112+
$this->logger->warning(
113+
\sprintf('Tool name "%s" is invalid. Tool names should only contain letters from a-z, numbers, dots, hyphens, underscores, and forward slashes.', $toolName),
114+
);
115+
}
116+
109117
$this->tools[$toolName] = new ToolReference($tool, $handler, $isManual);
110118

111119
$this->eventDispatcher?->dispatch(new ToolListChangedEvent());
@@ -118,7 +126,7 @@ public function registerResource(Resource $resource, callable|array|string $hand
118126

119127
if ($existing && !$isManual && $existing->isManual) {
120128
$this->logger->debug(
121-
"Ignoring discovered resource '{$uri}' as it conflicts with a manually registered one.",
129+
\sprintf('Ignoring discovered resource "%s" as it conflicts with a manually registered one.', $uri),
122130
);
123131

124132
return;
@@ -140,7 +148,7 @@ public function registerResourceTemplate(
140148

141149
if ($existing && !$isManual && $existing->isManual) {
142150
$this->logger->debug(
143-
"Ignoring discovered template '{$uriTemplate}' as it conflicts with a manually registered one.",
151+
\sprintf('Ignoring discovered template "%s" as it conflicts with a manually registered one.', $uriTemplate),
144152
);
145153

146154
return;
@@ -167,7 +175,7 @@ public function registerPrompt(
167175

168176
if ($existing && !$isManual && $existing->isManual) {
169177
$this->logger->debug(
170-
"Ignoring discovered prompt '{$promptName}' as it conflicts with a manually registered one.",
178+
\sprintf('Ignoring discovered prompt "%s" as it conflicts with a manually registered one.', $promptName),
171179
);
172180

173181
return;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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\Capability\Tool;
13+
14+
final class NameValidator
15+
{
16+
public function isValid(string $name): bool
17+
{
18+
return 1 === preg_match('/^[a-zA-Z0-9._\/-]{1,64}$/', $name);
19+
}
20+
}

tests/Inspector/InspectorSnapshotTestCase.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ protected function normalizeTestOutput(string $output, ?string $testName = null)
117117
return $output;
118118
}
119119

120-
/** @return array<string, array<string, mixed>> */
121120
public static function provideMethods(): array
122121
{
123122
return [

tests/Unit/Capability/Registry/RegistryTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public function testRegisterToolIgnoresDiscoveredWhenManualExists(): void
140140
$this->logger
141141
->expects($this->once())
142142
->method('debug')
143-
->with("Ignoring discovered tool 'test_tool' as it conflicts with a manually registered one.");
143+
->with('Ignoring discovered tool "test_tool" as it conflicts with a manually registered one.');
144144

145145
$this->registry->registerTool($discoveredTool, fn () => 'discovered', false);
146146

@@ -181,7 +181,7 @@ public function testRegisterResourceIgnoresDiscoveredWhenManualExists(): void
181181
$this->logger
182182
->expects($this->once())
183183
->method('debug')
184-
->with("Ignoring discovered resource 'test://resource' as it conflicts with a manually registered one.");
184+
->with('Ignoring discovered resource "test://resource" as it conflicts with a manually registered one.');
185185

186186
$this->registry->registerResource($discoveredResource, fn () => 'discovered', false);
187187

@@ -210,7 +210,7 @@ public function testRegisterResourceTemplateIgnoresDiscoveredWhenManualExists():
210210
$this->logger
211211
->expects($this->once())
212212
->method('debug')
213-
->with("Ignoring discovered template 'test://{id}' as it conflicts with a manually registered one.");
213+
->with('Ignoring discovered template "test://{id}" as it conflicts with a manually registered one.');
214214

215215
$this->registry->registerResourceTemplate($discoveredTemplate, fn () => 'discovered', [], false);
216216

@@ -239,7 +239,7 @@ public function testRegisterPromptIgnoresDiscoveredWhenManualExists(): void
239239
$this->logger
240240
->expects($this->once())
241241
->method('debug')
242-
->with("Ignoring discovered prompt 'test_prompt' as it conflicts with a manually registered one.");
242+
->with('Ignoring discovered prompt "test_prompt" as it conflicts with a manually registered one.');
243243

244244
$this->registry->registerPrompt($discoveredPrompt, fn () => 'discovered', [], false);
245245

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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\Tool;
13+
14+
use Mcp\Capability\Tool\NameValidator;
15+
use PHPUnit\Framework\Attributes\DataProvider;
16+
use PHPUnit\Framework\TestCase;
17+
18+
final class NameValidatorTest extends TestCase
19+
{
20+
#[DataProvider('provideValidNames')]
21+
public function testValidNames(string $name): void
22+
{
23+
$this->assertTrue((new NameValidator())->isValid($name));
24+
}
25+
26+
public static function provideValidNames(): array
27+
{
28+
return [
29+
['my_tool'],
30+
['MyTool123'],
31+
['my.tool'],
32+
['my-tool'],
33+
['my/tool'],
34+
['my_tool-01.02'],
35+
['my_long_toolname_that_is_exactly_sixty_four_characters_long_1234'],
36+
];
37+
}
38+
39+
#[DataProvider('provideInvalidNames')]
40+
public function testInvalidNames(string $name): void
41+
{
42+
$this->assertFalse((new NameValidator())->isValid($name));
43+
}
44+
45+
public static function provideInvalidNames(): array
46+
{
47+
return [
48+
[''],
49+
['my tool'],
50+
['my@tool'],
51+
['my!tool'],
52+
['my_tool#1'],
53+
['this_tool_name_is_way_too_long_because_it_exceeds_the_sixty_four_character_limit_set_by_the_validator'],
54+
];
55+
}
56+
}

0 commit comments

Comments
 (0)