Skip to content

Commit 5d598bf

Browse files
author
klapaudius
committed
Introduce ToolParamsValidator for tool argument validation
Added a ToolParamsValidator to centralize and validate tool input arguments using schemas. Integrated it into ToolsCallHandler and updated relevant tools by removing inline validation logic. Also introduced new exception classes and comprehensive test coverage for validation scenarios.
1 parent a8923fd commit 5d598bf

File tree

8 files changed

+283
-10
lines changed

8 files changed

+283
-10
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace KLP\KlpMcpServer\Exceptions;
4+
5+
use Exception;
6+
7+
class ToolParamsValidatorException extends Exception {
8+
public function __construct(string $message, private readonly array $errors) {
9+
parent::__construct($message);
10+
}
11+
12+
public function getErrors(): array {
13+
return $this->errors;
14+
}
15+
}

src/Protocol/MCPProtocol.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
namespace KLP\KlpMcpServer\Protocol;
44

55
use Exception;
6-
use Illuminate\Validation\ValidationException;
76
use KLP\KlpMcpServer\Data\Requests\NotificationData;
87
use KLP\KlpMcpServer\Data\Requests\RequestData;
98
use KLP\KlpMcpServer\Data\Resources\JsonRpc\JsonRpcErrorResource;
109
use KLP\KlpMcpServer\Data\Resources\JsonRpc\JsonRpcResultResource;
1110
use KLP\KlpMcpServer\Exceptions\Enums\JsonRpcErrorCode;
1211
use KLP\KlpMcpServer\Exceptions\JsonRpcErrorException;
12+
use KLP\KlpMcpServer\Exceptions\ToolParamsValidatorException;
1313
use KLP\KlpMcpServer\Protocol\Handlers\NotificationHandler;
1414
use KLP\KlpMcpServer\Protocol\Handlers\RequestHandler;
1515
use KLP\KlpMcpServer\Transports\TransportInterface;
@@ -142,8 +142,8 @@ private function handleRequestProcess(string $clientId, RequestData $requestData
142142
throw new JsonRpcErrorException("Method not found: {$requestData->method}", JsonRpcErrorCode::METHOD_NOT_FOUND);
143143
} catch (JsonRpcErrorException $e) {
144144
$this->pushMessage(clientId: $clientId, message: new JsonRpcErrorResource(exception: $e, id: $messageId));
145-
} catch (ValidationException $e) { // Todo remove this Undefined Exception
146-
$jsonRpcErrorException = new JsonRpcErrorException(message: $e->getMessage(), code: JsonRpcErrorCode::INVALID_PARAMS);
145+
} catch (ToolParamsValidatorException $e) {
146+
$jsonRpcErrorException = new JsonRpcErrorException(message: $e->getMessage() . implode( ',', $e->getErrors() ), code: JsonRpcErrorCode::INVALID_PARAMS);
147147
$this->pushMessage(clientId: $clientId, message: new JsonRpcErrorResource(exception: $jsonRpcErrorException, id: $messageId));
148148
} catch (Exception $e) {
149149
$jsonRpcErrorException = new JsonRpcErrorException(message: $e->getMessage(), code: JsonRpcErrorCode::INTERNAL_ERROR);

src/Server/Request/ToolsCallHandler.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
use KLP\KlpMcpServer\Exceptions\Enums\JsonRpcErrorCode;
66
use KLP\KlpMcpServer\Exceptions\JsonRpcErrorException;
77
use KLP\KlpMcpServer\Protocol\Handlers\RequestHandler;
8+
use KLP\KlpMcpServer\Services\ToolService\ToolParamsValidator;
89
use KLP\KlpMcpServer\Services\ToolService\ToolRepository;
10+
use KLP\KlpMcpServer\Exceptions\ToolParamsValidatorException;
911

1012
class ToolsCallHandler implements RequestHandler
1113
{
@@ -21,6 +23,15 @@ public function isHandle(string $method): bool
2123
return $method === 'tools/call' || $method === 'tools/execute';
2224
}
2325

26+
/**
27+
* Executes a specified method with provided parameters and returns the result.
28+
*
29+
* @param string $method The method to be executed.
30+
* @param array|null $params An associative array of parameters required for execution. Must include 'name' as the tool identifier and optionally 'arguments'.
31+
* @return array The response array containing the execution result, which may vary based on the method.
32+
* @throws JsonRpcErrorException If the tool name is missing or the tool is not found
33+
* @throws ToolParamsValidatorException If the provided arguments are invalid.
34+
*/
2435
public function execute(string $method, ?array $params = null): array
2536
{
2637
$name = $params['name'] ?? null;
@@ -35,6 +46,8 @@ public function execute(string $method, ?array $params = null): array
3546

3647
$arguments = $params['arguments'] ?? [];
3748

49+
ToolParamsValidator::validate($tool->getInputSchema(), $arguments);
50+
3851
$result = $tool->execute($arguments);
3952

4053
if ($method === 'tools/call') {

src/Services/ToolService/Examples/HelloWorldTool.php

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

33
namespace KLP\KlpMcpServer\Services\ToolService\Examples;
44

5-
use Illuminate\Support\Facades\Validator;
65
use KLP\KlpMcpServer\Services\ToolService\ToolInterface;
76

87
class HelloWorldTool implements ToolInterface
@@ -38,10 +37,6 @@ public function getAnnotations(): array
3837

3938
public function execute(array $arguments): string
4039
{
41-
Validator::make($arguments, [
42-
'name' => ['required', 'string'],
43-
])->validate();
44-
4540
$name = $arguments['name'] ?? 'MCP';
4641

4742
return "Hello, HelloWorld `{$name}` developer.";

src/Services/ToolService/Examples/VersionCheckTool.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace KLP\KlpMcpServer\Services\ToolService\Examples;
44

5-
use Illuminate\Support\Facades\App;
65
use KLP\KlpMcpServer\Services\ToolService\ToolInterface;
76
use stdClass;
87

@@ -34,7 +33,7 @@ public function getAnnotations(): array
3433

3534
public function execute(array $arguments): string
3635
{
37-
$now = now()->format('Y-m-d H:i:s');
36+
$now = (new \DateTime('now'))->format('Y-m-d H:i:s');
3837
$version = App::version();
3938

4039
return "current Version: {$version} - {$now}";

src/Services/ToolService/ToolInterface.php

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

55
interface ToolInterface
66
{
7+
/**
8+
* Retrieves the name.
9+
*
10+
* @return string The name.
11+
*/
712
public function getName(): string;
813

914
public function getDescription(): string;
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
namespace KLP\KlpMcpServer\Services\ToolService;
4+
5+
use KLP\KlpMcpServer\Exceptions\ToolParamsValidatorException;
6+
7+
class ToolParamsValidator
8+
{
9+
private static ?self $instance = null;
10+
private static array $errors = [];
11+
12+
13+
/**
14+
* The constructor method is private to restrict external instantiation of the class.
15+
*
16+
* @return void
17+
*/
18+
private function __construct() {}
19+
20+
public static function getErrors(): array
21+
{
22+
return self::$errors;
23+
}
24+
25+
/**
26+
* The clone method is private to prevent cloning of classes.
27+
*
28+
* @return void
29+
*/
30+
private function __clone() {}
31+
32+
/**
33+
* Provides a single instance of the class. If the instance does not already exist,
34+
* it initializes it and then returns it. Ensures that only one instance of the
35+
* class is created (Singleton pattern).
36+
*
37+
* @return self The single instance of the class.
38+
*/
39+
public static function getInstance(): self
40+
{
41+
return self::$instance ??= new self();
42+
}
43+
44+
45+
/**
46+
* Validates the provided arguments against the tool schema.
47+
*
48+
* @param array<string, mixed> $toolSchema The schema defining required arguments and their types.
49+
* @param array<string, mixed> $arguments The arguments to be validated.
50+
* @return void
51+
* @throws ToolParamsValidatorException if validation fails.
52+
*/
53+
public static function validate(array $toolSchema, array $arguments): void
54+
{
55+
self::getInstance();
56+
57+
$valid = true;
58+
foreach ($arguments as $argument => $value) {
59+
$test = isset($toolSchema['arguments'][$argument])
60+
&& self::validateType($toolSchema['arguments'][$argument]['type'], $value);
61+
if (!$test) {
62+
self::$errors[] = isset($toolSchema['arguments'][$argument])
63+
? "Invalid argument type for: $argument. Expected: {$toolSchema['arguments'][$argument]['type']}, got: " . gettype($value)
64+
: "Unknown argument: $argument";
65+
}
66+
$valid &= $test;
67+
}
68+
foreach ($toolSchema['required'] as $argument) {
69+
$test = !empty($arguments[$argument]);
70+
if (!$test) {
71+
self::$errors[] = "Missing required argument: $argument";
72+
}
73+
$valid &= $test;
74+
}
75+
76+
if (!$valid) {
77+
throw new ToolParamsValidatorException('Tool arguments validation failed.', self::$errors);
78+
}
79+
}
80+
81+
/**
82+
* Validates if the actual value matches the expected type.
83+
*
84+
* @param string $expectedType The expected data type (e.g., 'string', 'integer', 'boolean').
85+
* @param mixed $actualValue The value to be checked against the expected type.
86+
*
87+
* @return bool Returns true if the actual value matches the expected type; otherwise, returns false.
88+
*/
89+
private static function validateType(string $expectedType, mixed $actualValue): bool
90+
{
91+
return match ($expectedType) {
92+
'string' => is_string($actualValue),
93+
'integer' => is_int($actualValue),
94+
'boolean' => is_bool($actualValue),
95+
'array' => is_array($actualValue),
96+
'object' => is_object($actualValue),
97+
default => false
98+
};
99+
}
100+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
namespace KLP\KlpMcpServer\Tests\Services\ToolService;
4+
5+
use KLP\KlpMcpServer\Exceptions\ToolParamsValidatorException;
6+
use KLP\KlpMcpServer\Services\ToolService\ToolParamsValidator;
7+
use PHPUnit\Framework\TestCase;
8+
9+
/**
10+
* Tests for the ToolParamsValidator class and its validate method.
11+
*/
12+
class ToolParamsValidatorTest extends TestCase
13+
{
14+
public function test_validate_with_valid_arguments(): void
15+
{
16+
$toolSchema = [
17+
'arguments' => [
18+
'arg1' => ['type' => 'string'],
19+
'arg2' => ['type' => 'integer'],
20+
],
21+
'required' => ['arg1'],
22+
];
23+
24+
$arguments = [
25+
'arg1' => 'validString',
26+
'arg2' => 123,
27+
];
28+
29+
$this->expectNotToPerformAssertions();
30+
31+
ToolParamsValidator::validate($toolSchema, $arguments);
32+
}
33+
34+
public function test_validate_with_missing_required_argument(): void
35+
{
36+
$toolSchema = [
37+
'arguments' => [
38+
'arg1' => ['type' => 'string'],
39+
'arg2' => ['type' => 'integer'],
40+
],
41+
'required' => ['arg1', 'arg2'],
42+
];
43+
44+
$arguments = [
45+
'arg1' => 'validString',
46+
];
47+
48+
$this->expectException(ToolParamsValidatorException::class);
49+
$this->expectExceptionMessage('Tool arguments validation failed.');
50+
51+
try {
52+
ToolParamsValidator::validate($toolSchema, $arguments);
53+
} catch (ToolParamsValidatorException $exception) {
54+
$this->assertContains('Missing required argument: arg2', $exception->getErrors());
55+
throw $exception;
56+
}
57+
}
58+
59+
public function test_validate_with_empty_optional_argument(): void
60+
{
61+
$toolSchema = [
62+
'arguments' => [
63+
'arg1' => ['type' => 'string'],
64+
],
65+
'required' => [],
66+
];
67+
68+
$arguments = ['arg1' => ''];
69+
$this->expectNotToPerformAssertions();
70+
71+
ToolParamsValidator::validate($toolSchema, $arguments);
72+
}
73+
74+
public function test_validate_with_invalid_argument_not_in_schema(): void
75+
{
76+
$toolSchema = [
77+
'arguments' => [
78+
'arg1' => ['type' => 'string'],
79+
],
80+
'required' => ['arg1'],
81+
];
82+
83+
$arguments = [
84+
'arg1' => 'validString',
85+
'arg2' => 'extra',
86+
];
87+
88+
$this->expectException(ToolParamsValidatorException::class);
89+
$this->expectExceptionMessage('Tool arguments validation failed.');
90+
91+
try {
92+
ToolParamsValidator::validate($toolSchema, $arguments);
93+
} catch (ToolParamsValidatorException $exception) {
94+
$this->assertContains('Unknown argument: arg2', $exception->getErrors());
95+
throw $exception;
96+
}
97+
}
98+
99+
public function test_validate_with_invalid_argument_type(): void
100+
{
101+
$toolSchema = [
102+
'arguments' => [
103+
'arg1' => ['type' => 'string'],
104+
'arg2' => ['type' => 'integer'],
105+
],
106+
'required' => ['arg1', 'arg2'],
107+
];
108+
109+
$arguments = [
110+
'arg1' => 'validString',
111+
'arg2' => 'invalidType',
112+
];
113+
114+
$this->expectException(ToolParamsValidatorException::class);
115+
$this->expectExceptionMessage('Tool arguments validation failed.');
116+
117+
try {
118+
ToolParamsValidator::validate($toolSchema, $arguments);
119+
} catch (ToolParamsValidatorException $exception) {
120+
$this->assertContains('Invalid argument type for: arg2. Expected: integer, got: string', $exception->getErrors());
121+
throw $exception;
122+
}
123+
}
124+
125+
public function test_validate_with_empty_required_argument(): void
126+
{
127+
$toolSchema = [
128+
'arguments' => [
129+
'arg1' => ['type' => 'string'],
130+
],
131+
'required' => ['arg1'],
132+
];
133+
134+
$arguments = ['arg1' => ''];
135+
136+
$this->expectException(ToolParamsValidatorException::class);
137+
$this->expectExceptionMessage('Tool arguments validation failed.');
138+
139+
try {
140+
ToolParamsValidator::validate($toolSchema, $arguments);
141+
} catch (ToolParamsValidatorException $exception) {
142+
$this->assertContains('Missing required argument: arg1', $exception->getErrors());
143+
throw $exception;
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)