Skip to content

Commit c5d2241

Browse files
mowens3claude
andcommitted
Fix list schema rejecting complex objects in batch tools
The default items schema for data_type: 'list' without a ListInputDefinition was {"type": "string"}, which rejected complex objects like {"title": "...", "fields": {...}} sent by MCP clients for batch operations. Changed default to {} (unconstrained) so all batch tools accept complex items. Fixes #3572001 Reported-by: Guillaume Gérard (guillaumeg) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent df6bb5c commit c5d2241

3 files changed

Lines changed: 93 additions & 2 deletions

File tree

src/Mcp/ToolApiSchemaConverter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ private function inputDefinitionToSchema(InputDefinitionInterface $definition):
170170
// Provide best-effort list and map schemas when possible.
171171
if ($definition instanceof ListInputDefinition || $dataType === 'list' || $definition->isMultiple()) {
172172
$schema['type'] = 'array';
173-
$schema['items'] = ['type' => 'string'];
173+
$schema['items'] = new \stdClass();
174174
if ($definition instanceof ListInputDefinition) {
175175
$itemDefinition = $definition->getDataDefinition()->getItemDefinition();
176176
if ($itemDefinition instanceof InputDefinitionInterface) {

tests/src/Unit/Mcp/ToolApiSchemaConverterTest.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public function testToolDefinitionToInputSchemaMapsTypesAndConstraints(): void {
140140
$this->assertStringContainsString('Entity objects should be passed', $properties['ref']['description']);
141141

142142
$this->assertSame('array', $properties['tags']['type']);
143-
$this->assertSame('string', $properties['tags']['items']['type']);
143+
$this->assertInstanceOf(\stdClass::class, $properties['tags']['items']);
144144

145145
$this->assertSame('object', $properties['meta']['type']);
146146
}
@@ -165,6 +165,40 @@ public function testToolDefinitionToInputSchemaEncodesEmptyPropertiesAsObject():
165165
$this->assertSame('{}', json_encode($schema['properties']));
166166
}
167167

168+
public function testListWithoutItemDefinitionProducesUnconstrainedItemsSchema(): void {
169+
$converter = new ToolApiSchemaConverter();
170+
171+
$items = $this->mockInputDefinition(
172+
dataType: 'list',
173+
required: TRUE,
174+
description: 'Array of content items',
175+
constraints: [],
176+
);
177+
178+
$definition = new ToolDefinition([
179+
'id' => 'mcp_tools:batch_test',
180+
'provider' => 'mcp_tools',
181+
'label' => $this->markup('Batch Test'),
182+
'description' => $this->markup('Test batch tool'),
183+
'operation' => ToolOperation::Write,
184+
'destructive' => FALSE,
185+
'input_definitions' => [
186+
'items' => $items,
187+
],
188+
]);
189+
190+
$schema = $converter->toolDefinitionToInputSchema($definition);
191+
$properties = $schema['properties'];
192+
193+
// items must be typed as array.
194+
$this->assertSame('array', $properties['items']['type']);
195+
196+
// Without a ListInputDefinition, items schema must be unconstrained ({})
197+
// so complex objects like {"title": "...", "fields": {...}} are accepted.
198+
$this->assertInstanceOf(\stdClass::class, $properties['items']['items']);
199+
$this->assertSame('{}', json_encode($properties['items']['items']));
200+
}
201+
168202
private function mockInputDefinition(
169203
string $dataType,
170204
bool $required,

tests/src/Unit/Mcp/ToolInputValidatorTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,63 @@ public function testValidateReturnsErrorsForInvalidArrayItems(): void {
202202
$this->assertNotEmpty($result['errors']);
203203
}
204204

205+
public function testValidateAcceptsComplexObjectsInUnconstrainedArray(): void {
206+
$definition = $this->createMockDefinition();
207+
208+
// Simulates the schema produced for batch tools where data_type: 'list'
209+
// is used without a ListInputDefinition. The items schema must be {}
210+
// (unconstrained) so complex objects are accepted.
211+
$this->schemaConverter->method('toolDefinitionToInputSchema')
212+
->willReturn([
213+
'type' => 'object',
214+
'properties' => [
215+
'items' => [
216+
'type' => 'array',
217+
'items' => new \stdClass(),
218+
],
219+
],
220+
'required' => ['items'],
221+
]);
222+
223+
$validator = $this->createValidator();
224+
$result = $validator->validate($definition, [
225+
'items' => [
226+
['title' => 'Page one', 'fields' => ['body' => 'Content']],
227+
['title' => 'Page two', 'status' => TRUE],
228+
],
229+
]);
230+
231+
$this->assertTrue($result['valid']);
232+
$this->assertEmpty($result['errors']);
233+
}
234+
235+
public function testValidateRejectsComplexObjectsInStringConstrainedArray(): void {
236+
$definition = $this->createMockDefinition();
237+
238+
// When items is constrained to strings, complex objects must be rejected.
239+
$this->schemaConverter->method('toolDefinitionToInputSchema')
240+
->willReturn([
241+
'type' => 'object',
242+
'properties' => [
243+
'items' => [
244+
'type' => 'array',
245+
'items' => ['type' => 'string'],
246+
],
247+
],
248+
'required' => ['items'],
249+
]);
250+
251+
$validator = $this->createValidator();
252+
$result = $validator->validate($definition, [
253+
'items' => [
254+
['title' => 'Page one', 'fields' => ['body' => 'Content']],
255+
],
256+
]);
257+
258+
$this->assertFalse($result['valid']);
259+
$this->assertNotEmpty($result['errors']);
260+
}
261+
205262
public function testValidateReturnsValidForEmptyPropertiesSchema(): void {
206263
$definition = $this->createMockDefinition();
207264

0 commit comments

Comments
 (0)