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>
This commit is contained in:
Tobias Nyholm 2026-01-11 21:46:53 +01:00 • committed by GitHub
commit ed205ef793
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 103 additions and 29 deletions

View file

@ -220,7 +220,7 @@ class SchemaValidator
break;
case 'type':
$expected = implode('|', (array) ($args['expected'] ?? []));
$used = $args['used'] ?? 'unknown';
$used = $error->data()->type() ?? 'unknown';
$message = "Invalid type. Expected `{$expected}`, but received `{$used}`.";
break;
case 'enum':

View file

@ -91,9 +91,9 @@ class Error implements MessageInterface
return new self($id, self::METHOD_NOT_FOUND, $message);
}
public static function forInvalidParams(string $message, string|int $id = ''): self
public static function forInvalidParams(string $message, string|int $id = '', mixed $data = null): self
{
return new self($id, self::INVALID_PARAMS, $message);
return new self($id, self::INVALID_PARAMS, $message, $data);
}
public static function forInternalError(string $message, string|int $id = ''): self

View file

@ -11,6 +11,7 @@
namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Discovery\SchemaValidator;
use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\RegistryInterface;
use Mcp\Exception\ToolCallException;
@ -33,11 +34,15 @@ use Psr\Log\NullLogger;
*/
final class CallToolHandler implements RequestHandlerInterface
{
private SchemaValidator $schemaValidator;
public function __construct(
private readonly RegistryInterface $registry,
private readonly ReferenceHandlerInterface $referenceHandler,
private readonly LoggerInterface $logger = new NullLogger(),
?SchemaValidator $schemaValidator = null,
) {
$this->schemaValidator = $schemaValidator ?? new SchemaValidator($logger);
}
public function supports(Request $request): bool
@ -59,10 +64,35 @@ final class CallToolHandler implements RequestHandlerInterface
try {
$reference = $this->registry->getTool($toolName);
} catch (ToolNotFoundException $e) {
$this->logger->error('Tool not found', ['name' => $toolName]);
$arguments['_session'] = $session;
$arguments['_request'] = $request;
return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
}
$inputSchema = $reference->tool->inputSchema;
$validationErrors = $this->schemaValidator->validateAgainstJsonSchema($arguments, $inputSchema);
if (!empty($validationErrors)) {
$errorMessages = [];
foreach ($validationErrors as $errorDetail) {
$pointer = $errorDetail['pointer'] ?? '';
$message = $errorDetail['message'] ?? 'Unknown validation error';
$errorMessages[] = ('/' !== $pointer && '' !== $pointer ? "Property '{$pointer}': " : '').$message;
}
$summaryMessage = "Invalid parameters for tool '{$toolName}': ".implode('; ', \array_slice($errorMessages, 0, 3));
if (\count($errorMessages) > 3) {
$summaryMessage .= '; ...and more errors.';
}
return Error::forInvalidParams($summaryMessage, $request->getId(), ['validation_errors' => $validationErrors]);
}
$arguments['_session'] = $session;
$arguments['_request'] = $request;
try {
$result = $this->referenceHandler->handle($reference, $arguments);
$structuredContent = null;
@ -87,10 +117,6 @@ final class CallToolHandler implements RequestHandlerInterface
$errorContent = [new TextContent($e->getMessage())];
return new Response($request->getId(), CallToolResult::error($errorContent));
} catch (ToolNotFoundException $e) {
$this->logger->error('Tool not found', ['name' => $toolName]);
return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
} catch (\Throwable $e) {
$this->logger->error('Unhandled error during tool execution', [
'name' => $toolName,