[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:
parent
ff91ca6d60
commit
8e18bfbdf1
2 changed files with 86 additions and 8 deletions
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue