Skip to content

Commit ed205ef

Browse files
Nyholmchr-hertel
andauthored
Use SchemaValidator to validate tool input (#203)
* Use SchemaValidator to validate tool input * add test * add changelog * fix cs * Improve error message * fix tests * fix tests * fix baseline * update snapshot files * Remove BC compat while switching to v0.3.0 --------- Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
1 parent c0938a1 commit ed205ef

12 files changed

Lines changed: 103 additions & 29 deletions

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

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

5-
0.3
5+
0.3.0
66
-----
77

88
* Add output schema support to MCP tools
9+
* Add validation of the input parameters given to a Tool.
910

1011
0.2.2
1112
-----

examples/server/complex-tool-schema/Model/EventPriority.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
namespace Mcp\Example\Server\ComplexToolSchema\Model;
1313

14-
enum EventPriority: int
14+
enum EventPriority: string
1515
{
16-
case Low = 0;
17-
case Normal = 1;
18-
case High = 2;
16+
case Low = 'low';
17+
case Normal = 'normal';
18+
case High = 'high';
1919
}

src/Capability/Discovery/SchemaValidator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ private function formatValidationError(ValidationError $error): string
220220
break;
221221
case 'type':
222222
$expected = implode('|', (array) ($args['expected'] ?? []));
223-
$used = $args['used'] ?? 'unknown';
223+
$used = $error->data()->type() ?? 'unknown';
224224
$message = "Invalid type. Expected `{$expected}`, but received `{$used}`.";
225225
break;
226226
case 'enum':

src/Schema/JsonRpc/Error.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ public static function forMethodNotFound(string $message, string|int $id = ''):
9191
return new self($id, self::METHOD_NOT_FOUND, $message);
9292
}
9393

94-
public static function forInvalidParams(string $message, string|int $id = ''): self
94+
public static function forInvalidParams(string $message, string|int $id = '', mixed $data = null): self
9595
{
96-
return new self($id, self::INVALID_PARAMS, $message);
96+
return new self($id, self::INVALID_PARAMS, $message, $data);
9797
}
9898

9999
public static function forInternalError(string $message, string|int $id = ''): self

src/Server/Handler/Request/CallToolHandler.php

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Mcp\Server\Handler\Request;
1313

14+
use Mcp\Capability\Discovery\SchemaValidator;
1415
use Mcp\Capability\Registry\ReferenceHandlerInterface;
1516
use Mcp\Capability\RegistryInterface;
1617
use Mcp\Exception\ToolCallException;
@@ -33,11 +34,15 @@
3334
*/
3435
final class CallToolHandler implements RequestHandlerInterface
3536
{
37+
private SchemaValidator $schemaValidator;
38+
3639
public function __construct(
3740
private readonly RegistryInterface $registry,
3841
private readonly ReferenceHandlerInterface $referenceHandler,
3942
private readonly LoggerInterface $logger = new NullLogger(),
43+
?SchemaValidator $schemaValidator = null,
4044
) {
45+
$this->schemaValidator = $schemaValidator ?? new SchemaValidator($logger);
4146
}
4247

4348
public function supports(Request $request): bool
@@ -59,10 +64,35 @@ public function handle(Request $request, SessionInterface $session): Response|Er
5964

6065
try {
6166
$reference = $this->registry->getTool($toolName);
67+
} catch (ToolNotFoundException $e) {
68+
$this->logger->error('Tool not found', ['name' => $toolName]);
69+
70+
return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
71+
}
72+
73+
$inputSchema = $reference->tool->inputSchema;
74+
$validationErrors = $this->schemaValidator->validateAgainstJsonSchema($arguments, $inputSchema);
75+
if (!empty($validationErrors)) {
76+
$errorMessages = [];
77+
78+
foreach ($validationErrors as $errorDetail) {
79+
$pointer = $errorDetail['pointer'] ?? '';
80+
$message = $errorDetail['message'] ?? 'Unknown validation error';
81+
$errorMessages[] = ('/' !== $pointer && '' !== $pointer ? "Property '{$pointer}': " : '').$message;
82+
}
6283

63-
$arguments['_session'] = $session;
64-
$arguments['_request'] = $request;
84+
$summaryMessage = "Invalid parameters for tool '{$toolName}': ".implode('; ', \array_slice($errorMessages, 0, 3));
85+
if (\count($errorMessages) > 3) {
86+
$summaryMessage .= '; ...and more errors.';
87+
}
88+
89+
return Error::forInvalidParams($summaryMessage, $request->getId(), ['validation_errors' => $validationErrors]);
90+
}
6591

92+
$arguments['_session'] = $session;
93+
$arguments['_request'] = $request;
94+
95+
try {
6696
$result = $this->referenceHandler->handle($reference, $arguments);
6797

6898
$structuredContent = null;
@@ -87,10 +117,6 @@ public function handle(Request $request, SessionInterface $session): Response|Er
87117
$errorContent = [new TextContent($e->getMessage())];
88118

89119
return new Response($request->getId(), CallToolResult::error($errorContent));
90-
} catch (ToolNotFoundException $e) {
91-
$this->logger->error('Tool not found', ['name' => $toolName]);
92-
93-
return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
94120
} catch (\Throwable $e) {
95121
$this->logger->error('Unhandled error during tool execution', [
96122
'name' => $toolName,

tests/Inspector/Http/HttpDiscoveryUserProfileTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static function provideMethods(): array
2121
'method' => 'tools/call',
2222
'options' => [
2323
'toolName' => 'send_welcome',
24-
'toolArgs' => ['userId' => '101', 'customMessage' => 'Welcome to our platform!'],
24+
'toolArgs' => ['userId' => '"101"', 'customMessage' => 'Welcome to our platform!'],
2525
],
2626
'testName' => 'send_welcome',
2727
],

tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_all_day_reminder.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"content": [
33
{
44
"type": "text",
5-
"text": "{\n \"success\": true,\n \"message\": \"Event \\\"Project Deadline\\\" scheduled successfully for \\\"2024-12-15\\\".\",\n \"event_details\": {\n \"title\": \"Project Deadline\",\n \"date\": \"2024-12-15\",\n \"type\": \"reminder\",\n \"time\": \"All day\",\n \"priority\": \"Normal\",\n \"attendees\": [],\n \"invites_will_be_sent\": false\n }\n}"
5+
"text": "{\n \"success\": true,\n \"message\": \"Event \\\"Project Deadline\\\" scheduled successfully for \\\"2024-12-15\\\".\",\n \"event_details\": {\n \"title\": \"Project Deadline\",\n \"date\": \"2024-12-15\",\n \"type\": \"reminder\",\n \"time\": \"All day\",\n \"priority\": \"High\",\n \"attendees\": [],\n \"invites_will_be_sent\": false\n }\n}"
66
}
77
],
88
"structuredContent": {
@@ -13,7 +13,7 @@
1313
"date": "2024-12-15",
1414
"type": "reminder",
1515
"time": "All day",
16-
"priority": "Normal",
16+
"priority": "High",
1717
"attendees": [],
1818
"invites_will_be_sent": false
1919
}

tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_high_priority.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"content": [
33
{
44
"type": "text",
5-
"text": "{\n \"success\": true,\n \"message\": \"Event \\\"Client Call\\\" scheduled successfully for \\\"2024-12-02\\\".\",\n \"event_details\": {\n \"title\": \"Client Call\",\n \"date\": \"2024-12-02\",\n \"type\": \"call\",\n \"time\": \"14:30\",\n \"priority\": \"Normal\",\n \"attendees\": [\n \"client@example.com\"\n ],\n \"invites_will_be_sent\": false\n }\n}"
5+
"text": "{\n \"success\": true,\n \"message\": \"Event \\\"Client Call\\\" scheduled successfully for \\\"2024-12-02\\\".\",\n \"event_details\": {\n \"title\": \"Client Call\",\n \"date\": \"2024-12-02\",\n \"type\": \"call\",\n \"time\": \"14:30\",\n \"priority\": \"High\",\n \"attendees\": [\n \"client@example.com\"\n ],\n \"invites_will_be_sent\": false\n }\n}"
66
}
77
],
88
"structuredContent": {
@@ -13,7 +13,7 @@
1313
"date": "2024-12-02",
1414
"type": "call",
1515
"time": "14:30",
16-
"priority": "Normal",
16+
"priority": "High",
1717
"attendees": [
1818
"client@example.com"
1919
],

tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_low_priority.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"content": [
33
{
44
"type": "text",
5-
"text": "{\n \"success\": true,\n \"message\": \"Event \\\"Office Party\\\" scheduled successfully for \\\"2024-12-20\\\".\",\n \"event_details\": {\n \"title\": \"Office Party\",\n \"date\": \"2024-12-20\",\n \"type\": \"other\",\n \"time\": \"18:00\",\n \"priority\": \"Normal\",\n \"attendees\": [\n \"team@company.com\"\n ],\n \"invites_will_be_sent\": true\n }\n}"
5+
"text": "{\n \"success\": true,\n \"message\": \"Event \\\"Office Party\\\" scheduled successfully for \\\"2024-12-20\\\".\",\n \"event_details\": {\n \"title\": \"Office Party\",\n \"date\": \"2024-12-20\",\n \"type\": \"other\",\n \"time\": \"18:00\",\n \"priority\": \"Low\",\n \"attendees\": [\n \"team@company.com\"\n ],\n \"invites_will_be_sent\": true\n }\n}"
66
}
77
],
88
"structuredContent": {
@@ -13,7 +13,7 @@
1313
"date": "2024-12-20",
1414
"type": "other",
1515
"time": "18:00",
16-
"priority": "Normal",
16+
"priority": "Low",
1717
"attendees": [
1818
"team@company.com"
1919
],

tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_list.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@
3333
"default": null
3434
},
3535
"priority": {
36-
"type": "integer",
36+
"type": "string",
3737
"description": "The priority of the event. Defaults to Normal.",
38-
"default": 1,
38+
"default": "normal",
3939
"enum": [
40-
0,
41-
1,
42-
2
40+
"low",
41+
"normal",
42+
"high"
4343
]
4444
},
4545
"attendees": {

0 commit comments

Comments
 (0)