Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
* [BC Break] Reject a `Tool` input schema whose `properties` is not an object or whose `required` is neither a list nor `null`, instead of silently replacing the member. Reject a `completion/complete` whose `argument` is missing `name` or `value`, instead of completing against an empty prefix.
* Add `HttpTransport::getSessionId()` to read the server-minted `Mcp-Session-Id`: a request-scoped caller can persist it and pass it back through the constructor's `$headers` on a later transport. Always `null` on `2026-07-28`, which removed protocol-level sessions.
* Fix `HttpTransport` omitting the `MCP-Protocol-Version` header on requests after the `initialize` handshake, including the closing `DELETE`.
* Fix memory growing with every `tools/call` in long-lived processes like FrankenPHP workers or stdio servers.
* Fix OIDC discovery rejecting issuers with a trailing slash (e.g. Authentik, Auth0).
* Fix `Client::connect()` rejecting an empty `serverInfo.version`, as the Python SDK sends, and leaving the transport open when initialization fails on a malformed result: the transport is now closed and the failure wrapped in a `ConnectionException`.
* Fix stateless SSE streams holding back frames until close when PHP output buffering is enabled.
Expand Down
23 changes: 8 additions & 15 deletions src/Capability/Discovery/SchemaValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

namespace Mcp\Capability\Discovery;

use Mcp\Exception\InvalidArgumentException;
use Opis\JsonSchema\Errors\ValidationError;
use Opis\JsonSchema\Validator;
use Psr\Log\LoggerInterface;
Expand All @@ -30,6 +29,11 @@
*/
class SchemaValidator
{
/**
* @var array<string, mixed>
*/
private array $decodedSchemas = [];

public function __construct(
private ?Validator $jsonSchemaValidator = null,
private LoggerInterface $logger = new NullLogger(),
Expand All @@ -48,16 +52,9 @@ public function validateAgainstJsonSchema(mixed $data, array|object $schema): ar
{
try {
// --- Schema Preparation ---
if (\is_array($schema)) {
$schemaJson = json_encode($schema, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES);
$schemaObject = json_decode($schemaJson, false, 512, \JSON_THROW_ON_ERROR);
} elseif (\is_object($schema)) {
// This might be overly cautious but safer against varied inputs.
$schemaJson = json_encode($schema, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES);
$schemaObject = json_decode($schemaJson, false, 512, \JSON_THROW_ON_ERROR);
} else {
throw new InvalidArgumentException('Schema must be an array or object.');
}
// Opis caches every schema object it sees for good, so the same schema has to map to the same object.
$schemaJson = json_encode($schema, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES);
$schemaObject = $this->decodedSchemas[$schemaJson] ??= json_decode($schemaJson, false, 512, \JSON_THROW_ON_ERROR);

// --- Data Preparation ---
// Opis Validator generally prefers objects for object validation
Expand All @@ -66,10 +63,6 @@ public function validateAgainstJsonSchema(mixed $data, array|object $schema): ar
$this->logger->error('MCP SDK: Invalid schema structure provided for validation (JSON conversion failed).', ['exception' => $e]);

return [['pointer' => '', 'keyword' => 'internal', 'message' => 'Invalid schema definition provided (JSON error).']];
} catch (InvalidArgumentException $e) {
$this->logger->error('MCP SDK: Invalid schema structure provided for validation.', ['exception' => $e]);

return [['pointer' => '', 'keyword' => 'internal', 'message' => $e->getMessage()]];
} catch (\Throwable $e) {
$this->logger->error('MCP SDK: Error preparing data/schema for validation.', ['exception' => $e]);

Expand Down
24 changes: 24 additions & 0 deletions tests/Unit/Capability/Discovery/SchemaValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

use Mcp\Capability\Attribute\Schema;
use Mcp\Capability\Discovery\SchemaValidator;
use Opis\JsonSchema\SchemaLoader;
use Opis\JsonSchema\Validator;
use PHPUnit\Framework\TestCase;

class SchemaValidatorTest extends TestCase
Expand All @@ -24,6 +26,28 @@ protected function setUp(): void
$this->validator = new SchemaValidator();
}

public function testRepeatedValidationDoesNotGrowSchemaLoaderCache(): void
{
$loader = new class extends SchemaLoader {
public function cacheSize(): int
{
return \count($this->dataCache) + \count($this->uriCache);
}
};
$validator = new SchemaValidator(new Validator($loader));

$validator->validateAgainstJsonSchema($this->getValidData(), $this->getSimpleSchema());
$validator->validateAgainstJsonSchema($this->getValidData(), (object) $this->getSimpleSchema());
$size = $loader->cacheSize();

for ($i = 0; $i < 10; ++$i) {
$validator->validateAgainstJsonSchema($this->getValidData(), $this->getSimpleSchema());
$validator->validateAgainstJsonSchema($this->getValidData(), (object) $this->getSimpleSchema());
}

$this->assertSame($size, $loader->cacheSize());
}

// --- Basic Validation Tests ---

public function testValidDataPassesValidation(): void
Expand Down
Loading