From 2bef7727f30a44b2fb9fea888e7c759dadb61512 Mon Sep 17 00:00:00 2001 From: Hanshuk Date: Tue, 11 Aug 2026 00:41:19 +0530 Subject: [PATCH 1/4] fix: normalize empty tool schema properties --- src/Schema/Tool.php | 24 ++++-- tests/Unit/Schema/ToolTest.php | 142 +++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 5 deletions(-) diff --git a/src/Schema/Tool.php b/src/Schema/Tool.php index 18178044..f71c0e50 100644 --- a/src/Schema/Tool.php +++ b/src/Schema/Tool.php @@ -127,7 +127,7 @@ public function jsonSerialize(): array if (null !== $this->title) { $data['title'] = $this->title; } - $data['inputSchema'] = $this->inputSchema; + $data['inputSchema'] = self::normalizeSchemaProperties($this->inputSchema); if (null !== $this->description) { $data['description'] = $this->description; } @@ -141,14 +141,14 @@ public function jsonSerialize(): array $data['_meta'] = $this->meta; } if (null !== $this->outputSchema) { - $data['outputSchema'] = $this->outputSchema; + $data['outputSchema'] = self::normalizeSchemaProperties($this->outputSchema); } return $data; } /** - * Normalize schema properties: convert an empty properties array to stdClass. + * Normalize schema properties: convert an empty properties array to stdClass recursively. * * @param array $schema * @@ -156,8 +156,22 @@ public function jsonSerialize(): array */ private static function normalizeSchemaProperties(array $schema): array { - if (isset($schema['properties']) && \is_array($schema['properties']) && empty($schema['properties'])) { - $schema['properties'] = new \stdClass(); + if (isset($schema['properties']) && \is_array($schema['properties'])) { + if (empty($schema['properties'])) { + $schema['properties'] = new \stdClass(); + } else { + foreach ($schema['properties'] as $key => $propSchema) { + if (\is_array($propSchema)) { + $schema['properties'][$key] = self::normalizeSchemaProperties($propSchema); + } + } + } + } + + foreach ($schema as $key => $value) { + if ($key !== 'properties' && \is_array($value)) { + $schema[$key] = self::normalizeSchemaProperties($value); + } } return $schema; diff --git a/tests/Unit/Schema/ToolTest.php b/tests/Unit/Schema/ToolTest.php index dd6861c3..4351f350 100644 --- a/tests/Unit/Schema/ToolTest.php +++ b/tests/Unit/Schema/ToolTest.php @@ -97,4 +97,146 @@ public function testRoundTripPreservesTitle(): void $this->assertSame($original->name, $restored->name); $this->assertSame($original->description, $restored->description); } + + public function testEmptyPropertiesNormalizationOnDirectConstruction(): void + { + $tool = new Tool( + name: 'test', + title: null, + inputSchema: [ + 'type' => 'object', + 'properties' => [], + ], + description: null, + annotations: null, + ); + $serialized = $tool->jsonSerialize(); + $this->assertInstanceOf(\stdClass::class, $serialized['inputSchema']['properties']); + $this->assertSame('{"name":"test","inputSchema":{"type":"object","properties":{}}}', json_encode($serialized)); + } + + public function testEmptyPropertiesNormalizationOnFromArray(): void + { + $tool = Tool::fromArray([ + 'name' => 'test', + 'inputSchema' => [ + 'type' => 'object', + 'properties' => [], + ], + ]); + $serialized = $tool->jsonSerialize(); + $this->assertInstanceOf(\stdClass::class, $serialized['inputSchema']['properties']); + $this->assertSame('{"name":"test","inputSchema":{"type":"object","properties":{}}}', json_encode($serialized)); + } + + public function testJsonRoundTripPreservesEmptyPropertiesObject(): void + { + $json = '{"type":"object","properties":{}}'; + $schema = json_decode($json, true); + $tool = new Tool('test', null, $schema, null, null); + $serialized = $tool->jsonSerialize(); + + $this->assertInstanceOf(\stdClass::class, $serialized['inputSchema']['properties']); + $this->assertSame('{"name":"test","inputSchema":{"type":"object","properties":{}}}', json_encode($serialized)); + } + + public function testNestedObjectEmptyPropertiesNormalization(): void + { + $tool = new Tool( + name: 'test', + title: null, + inputSchema: [ + 'type' => 'object', + 'properties' => [ + 'filter' => [ + 'type' => 'object', + 'properties' => [], + ], + ], + ], + description: null, + annotations: null, + ); + $serialized = $tool->jsonSerialize(); + $this->assertInstanceOf(\stdClass::class, $serialized['inputSchema']['properties']['filter']['properties']); + $this->assertSame('{"name":"test","inputSchema":{"type":"object","properties":{"filter":{"type":"object","properties":{}}}}}', json_encode($serialized)); + } + + public function testExistingNonEmptyPropertiesUnchanged(): void + { + $tool = new Tool( + name: 'test', + title: null, + inputSchema: [ + 'type' => 'object', + 'properties' => [ + 'name' => [ + 'type' => 'string', + ], + ], + ], + description: null, + annotations: null, + ); + $serialized = $tool->jsonSerialize(); + $this->assertIsArray($serialized['inputSchema']['properties']); + $this->assertArrayHasKey('name', $serialized['inputSchema']['properties']); + $this->assertSame('{"name":"test","inputSchema":{"type":"object","properties":{"name":{"type":"string"}}}}', json_encode($serialized)); + } + + public function testValidArraysAreNotTransformed(): void + { + $tool = new Tool( + name: 'test', + title: null, + inputSchema: [ + 'type' => 'object', + 'properties' => [ + 'tags' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'string' + ], + ], + 'status' => [ + 'type' => 'string', + 'enum' => [], + ], + ], + 'required' => [], + ], + description: null, + annotations: null, + ); + $serialized = $tool->jsonSerialize(); + $this->assertIsArray($serialized['inputSchema']['required']); + $this->assertEmpty($serialized['inputSchema']['required']); + $this->assertIsArray($serialized['inputSchema']['properties']['tags']); + $this->assertIsArray($serialized['inputSchema']['properties']['status']['enum']); + $this->assertEmpty($serialized['inputSchema']['properties']['status']['enum']); + $this->assertSame('{"name":"test","inputSchema":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":[]}},"required":[]}}', json_encode($serialized)); + } + + public function testOutputSchemaEmptyPropertiesNormalization(): void + { + $tool = new Tool( + name: 'test', + title: null, + inputSchema: [ + 'type' => 'object', + 'properties' => ['q' => ['type' => 'string']], + ], + description: null, + annotations: null, + icons: null, + meta: null, + outputSchema: [ + 'type' => 'object', + 'properties' => [], + ] + ); + $serialized = $tool->jsonSerialize(); + $this->assertInstanceOf(\stdClass::class, $serialized['outputSchema']['properties']); + $this->assertSame('{"name":"test","inputSchema":{"type":"object","properties":{"q":{"type":"string"}}},"outputSchema":{"type":"object","properties":{}}}', json_encode($serialized)); + } } From 039584e0964385bd98c710c909110e53a4994fea Mon Sep 17 00:00:00 2001 From: Hanshuk Date: Tue, 11 Aug 2026 01:10:12 +0530 Subject: [PATCH 2/4] test: fix tool schema regression test QA --- tests/Unit/Schema/ToolTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Unit/Schema/ToolTest.php b/tests/Unit/Schema/ToolTest.php index 4351f350..59988267 100644 --- a/tests/Unit/Schema/ToolTest.php +++ b/tests/Unit/Schema/ToolTest.php @@ -132,7 +132,7 @@ public function testEmptyPropertiesNormalizationOnFromArray(): void public function testJsonRoundTripPreservesEmptyPropertiesObject(): void { $json = '{"type":"object","properties":{}}'; - $schema = json_decode($json, true); + $schema = (array) json_decode($json, true); $tool = new Tool('test', null, $schema, null, null); $serialized = $tool->jsonSerialize(); @@ -195,7 +195,7 @@ public function testValidArraysAreNotTransformed(): void 'tags' => [ 'type' => 'array', 'items' => [ - 'type' => 'string' + 'type' => 'string', ], ], 'status' => [ @@ -233,7 +233,7 @@ public function testOutputSchemaEmptyPropertiesNormalization(): void outputSchema: [ 'type' => 'object', 'properties' => [], - ] + ], ); $serialized = $tool->jsonSerialize(); $this->assertInstanceOf(\stdClass::class, $serialized['outputSchema']['properties']); From cf8f194cf3aafbd6719e73393fb0023a3ffc4c94 Mon Sep 17 00:00:00 2001 From: Hanshuk Date: Tue, 11 Aug 2026 01:15:02 +0530 Subject: [PATCH 3/4] style: fix schema normalization condition --- src/Schema/Tool.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schema/Tool.php b/src/Schema/Tool.php index f71c0e50..fd1e4446 100644 --- a/src/Schema/Tool.php +++ b/src/Schema/Tool.php @@ -169,7 +169,7 @@ private static function normalizeSchemaProperties(array $schema): array } foreach ($schema as $key => $value) { - if ($key !== 'properties' && \is_array($value)) { + if ('properties' !== $key && \is_array($value)) { $schema[$key] = self::normalizeSchemaProperties($value); } } From 1c89b1858e1f78d913813a4f681d05320f68aa76 Mon Sep 17 00:00:00 2001 From: Hanshuk Date: Tue, 11 Aug 2026 01:21:43 +0530 Subject: [PATCH 4/4] fix: align tool input schema type --- src/Schema/Tool.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Schema/Tool.php b/src/Schema/Tool.php index fd1e4446..304defb1 100644 --- a/src/Schema/Tool.php +++ b/src/Schema/Tool.php @@ -21,8 +21,8 @@ * * @phpstan-type ToolInputSchema array{ * type: 'object', - * properties: array, - * required: string[]|null + * properties?: array, + * required?: string[]|null * } * @phpstan-type ToolOutputSchema array{ * type: 'object',