[Server] Always emit items for array tool parameter schemas (#378)

Array tool parameters without an inferable element type generated `{"type":"array"}` with no `items`, which strict MCP clients (VS Code / Copilot) reject. Always emit `items` (`{}` when the element type is unknown) and recover the element type for nullable typed arrays such as `string[]|null`.

Fixes #151
This commit is contained in:
Valeriu Gutu 2026-07-15 00:56:50 +03:00 • committed by GitHub
commit 8e18bfbdf1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 86 additions and 8 deletions

View file

@ -266,6 +266,18 @@ class SchemaGeneratorFixture
): void {
}
/**
* Nullable typed arrays should still recover their element type.
*
* @param string[]|null $nullableStrings Nullable list of strings
* @param array<int>|null $nullableInts Nullable list of integers
*/
public function nullableTypedArrays(
?array $nullableStrings,
?array $nullableInts = null,
): void {
}
// ===== NULLABLE AND OPTIONAL SCENARIOS =====
/**
@ -308,6 +320,15 @@ class SchemaGeneratorFixture
{
}
/**
* Variadic parameter without a type hint.
*
* @param mixed ...$values Variadic values
*/
public function untypedVariadic(...$values): void
{
}
/**
* Variadic with Schema constraints.
*

View file

@ -44,7 +44,7 @@ final class SchemaGeneratorTest extends TestCase
$this->assertEquals(['type' => 'string'], $schema['properties']['name']);
$this->assertEquals(['type' => 'integer'], $schema['properties']['age']);
$this->assertEquals(['type' => 'boolean'], $schema['properties']['active']);
$this->assertEquals(['type' => 'array'], $schema['properties']['tags']);
$this->assertEquals(['type' => 'array', 'items' => new \stdClass()], $schema['properties']['tags']);
$this->assertEquals(['type' => ['null', 'object'], 'default' => null], $schema['properties']['config']);
$this->assertEqualsCanonicalizing(['name', 'age', 'active', 'tags'], $schema['required']);
}
@ -56,7 +56,7 @@ final class SchemaGeneratorTest extends TestCase
$this->assertEquals(['type' => 'string', 'description' => 'The username'], $schema['properties']['username']);
$this->assertEquals(['type' => 'integer', 'description' => 'Number of items'], $schema['properties']['count']);
$this->assertEquals(['type' => 'boolean', 'description' => 'Whether enabled'], $schema['properties']['enabled']);
$this->assertEquals(['type' => 'array', 'description' => 'Some data'], $schema['properties']['data']);
$this->assertEquals(['type' => 'array', 'description' => 'Some data', 'items' => new \stdClass()], $schema['properties']['data']);
$this->assertEqualsCanonicalizing(['username', 'count', 'enabled', 'data'], $schema['required']);
}
@ -206,10 +206,13 @@ final class SchemaGeneratorTest extends TestCase
{
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'arrayTypeScenarios');
$schema = $this->schemaGenerator->generate($method);
$this->assertEquals(['type' => 'array', 'description' => 'Generic array'], $schema['properties']['genericArray']);
$this->assertEquals(['type' => 'array', 'description' => 'Generic array', 'items' => new \stdClass()], $schema['properties']['genericArray']);
// An untyped array must still declare `items`, serialized as the empty schema `{}`
// (not `[]`) so strict clients accept it.
$this->assertSame('{}', json_encode($schema['properties']['genericArray']['items']));
$this->assertEquals(['type' => 'array', 'description' => 'Array of strings', 'items' => ['type' => 'string']], $schema['properties']['stringArray']);
$this->assertEquals(['type' => 'array', 'description' => 'Array of integers', 'items' => ['type' => 'integer']], $schema['properties']['intArray']);
$this->assertEquals(['type' => 'array', 'description' => 'Mixed array map'], $schema['properties']['mixedMap']);
$this->assertEquals(['type' => 'array', 'description' => 'Mixed array map', 'items' => new \stdClass()], $schema['properties']['mixedMap']);
$this->assertArrayHasKey('type', $schema['properties']['objectLikeArray']);
$this->assertEquals('object', $schema['properties']['objectLikeArray']['type']);
$this->assertArrayHasKey('properties', $schema['properties']['objectLikeArray']);
@ -218,6 +221,16 @@ final class SchemaGeneratorTest extends TestCase
$this->assertEqualsCanonicalizing(['genericArray', 'stringArray', 'intArray', 'mixedMap', 'objectLikeArray', 'nestedObjectArray'], $schema['required']);
}
public function testRecoversItemsTypeForNullableTypedArrays(): void
{
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'nullableTypedArrays');
$schema = $this->schemaGenerator->generate($method);
// A `|null` suffix must not erase the element type: `string[]|null` keeps `items: {type: string}`.
$this->assertEquals(['type' => ['array', 'null'], 'description' => 'Nullable list of strings', 'items' => ['type' => 'string']], $schema['properties']['nullableStrings']);
$this->assertEquals(['type' => ['array', 'null'], 'description' => 'Nullable list of integers', 'default' => null, 'items' => ['type' => 'integer']], $schema['properties']['nullableInts']);
$this->assertEqualsCanonicalizing(['nullableStrings'], $schema['required']);
}
public function testHandlesNullableTypeHintsAndOptionalParameters(): void
{
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'nullableAndOptional');
@ -226,7 +239,7 @@ final class SchemaGeneratorTest extends TestCase
$this->assertEquals(['type' => ['null', 'integer'], 'description' => 'Nullable integer', 'default' => null], $schema['properties']['nullableInt']);
$this->assertEquals(['type' => 'string', 'default' => 'default'], $schema['properties']['optionalString']);
$this->assertEquals(['type' => 'boolean', 'default' => true], $schema['properties']['optionalBool']);
$this->assertEquals(['type' => 'array', 'default' => []], $schema['properties']['optionalArray']);
$this->assertEquals(['type' => 'array', 'default' => [], 'items' => new \stdClass()], $schema['properties']['optionalArray']);
$this->assertEqualsCanonicalizing(['nullableString'], $schema['required']);
}
@ -255,6 +268,14 @@ final class SchemaGeneratorTest extends TestCase
$this->assertArrayNotHasKey('required', $schema);
}
public function testUntypedVariadicStillDeclaresItems(): void
{
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'untypedVariadic');
$schema = $this->schemaGenerator->generate($method);
$this->assertEquals(['type' => 'array', 'description' => 'Variadic values', 'items' => new \stdClass()], $schema['properties']['values']);
$this->assertSame('{}', json_encode($schema['properties']['values']['items']));
}
public function testHandlesMixedTypeHintsOmittingExplicitType(): void
{
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'mixedTypes');