Skip to content

Commit 4e96598

Browse files
committed
feat: Add output schema support to MCP tools
1 parent 2772869 commit 4e96598

File tree

4 files changed

+23
-233
lines changed

4 files changed

+23
-233
lines changed

src/Capability/Registry/ToolReference.php

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -126,35 +126,17 @@ public function extractStructuredContent(mixed $toolExecutionResult): ?array
126126
return null;
127127
}
128128

129-
// If outputSchema has properties.result, wrap in result key
130-
if (isset($outputSchema['properties']['result'])) {
131-
return ['result' => $this->normalizeValue($toolExecutionResult)];
132-
}
133-
134-
if (isset($outputSchema['additionalProperties'])) {
135-
if (\is_array($toolExecutionResult)) {
136-
// Check if it's a numeric array (list)
137-
if (array_is_list($toolExecutionResult)) {
138-
// Wrap list in "object" schema
139-
return ['items' => $toolExecutionResult];
140-
}
141-
142-
return $toolExecutionResult;
129+
if (\is_array($toolExecutionResult)) {
130+
if (array_is_list($toolExecutionResult) && isset($outputSchema['additionalProperties'])) {
131+
// Wrap list in "object" schema for additionalProperties
132+
return ['items' => $toolExecutionResult];
143133
}
144134

145-
if (\is_object($toolExecutionResult) && !($toolExecutionResult instanceof Content)) {
146-
return $this->normalizeValue($toolExecutionResult);
147-
}
135+
return $toolExecutionResult;
148136
}
149137

150-
if (isset($outputSchema['properties'])) {
151-
if (\is_array($toolExecutionResult)) {
152-
return $toolExecutionResult;
153-
}
154-
155-
if (\is_object($toolExecutionResult) && !($toolExecutionResult instanceof Content)) {
156-
return $this->normalizeValue($toolExecutionResult);
157-
}
138+
if (\is_object($toolExecutionResult) && !($toolExecutionResult instanceof Content)) {
139+
return $this->normalizeValue($toolExecutionResult);
158140
}
159141

160142
return null;

tests/Unit/Capability/Discovery/SchemaGeneratorFixture.php

Lines changed: 4 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -431,126 +431,14 @@ public function parameterSchemaInferredType(
431431
outputSchema: [
432432
'type' => 'object',
433433
'properties' => [
434-
'result' => ['type' => 'string'],
434+
'message' => ['type' => 'string'],
435435
],
436-
'required' => ['result'],
437-
]
438-
)]
439-
public function stringReturn(): string
440-
{
441-
return 'test';
442-
}
443-
444-
#[McpTool(
445-
outputSchema: [
446-
'type' => 'object',
447-
'properties' => [
448-
'result' => ['type' => 'integer'],
449-
],
450-
'required' => ['result'],
451-
]
452-
)]
453-
public function integerReturn(): int
454-
{
455-
return 42;
456-
}
457-
458-
#[McpTool(
459-
outputSchema: [
460-
'type' => 'object',
461-
'properties' => [
462-
'result' => ['type' => 'number'],
463-
],
464-
'required' => ['result'],
465-
]
466-
)]
467-
public function floatReturn(): float
468-
{
469-
return 3.14;
470-
}
471-
472-
#[McpTool(
473-
outputSchema: [
474-
'type' => 'object',
475-
'properties' => [
476-
'result' => ['type' => 'boolean'],
477-
],
478-
'required' => ['result'],
479-
]
480-
)]
481-
public function booleanReturn(): bool
482-
{
483-
return true;
484-
}
485-
486-
#[McpTool(
487-
outputSchema: [
488-
'type' => 'object',
489-
'properties' => [
490-
'result' => ['type' => 'string'],
491-
],
492-
'required' => ['result'],
493-
]
494-
)]
495-
public function nullableReturn(): ?string
496-
{
497-
return null;
498-
}
499-
500-
#[McpTool(
501-
outputSchema: [
502-
'type' => 'object',
503-
'properties' => [
504-
'result' => ['type' => 'object'],
505-
],
506-
'required' => ['result'],
507-
]
508-
)]
509-
public function objectReturn(): \stdClass
510-
{
511-
return new \stdClass();
512-
}
513-
514-
#[McpTool(
515-
outputSchema: [
516-
'type' => 'object',
517-
'properties' => [
518-
'result' => ['type' => 'string'],
519-
],
520-
'required' => ['result'],
521-
]
522-
)]
523-
public function docBlockReturnType(): string
524-
{
525-
return '42';
526-
}
527-
528-
#[McpTool(
529-
outputSchema: [
530-
'type' => 'object',
531-
'properties' => [
532-
'result' => ['type' => 'string'],
533-
],
534-
'required' => ['result'],
436+
'required' => ['message'],
535437
'description' => 'The result of the operation',
536438
]
537439
)]
538-
public function returnWithExplicitOutputSchema(): string
539-
{
540-
return 'result';
541-
}
542-
543-
#[McpTool(
544-
outputSchema: [
545-
'type' => 'object',
546-
'properties' => [
547-
'result' => ['type' => ['string', 'number']],
548-
],
549-
'required' => ['result'],
550-
]
551-
)]
552-
public function unionReturnWithExplicitOutputSchema(): float|string
440+
public function returnWithExplicitOutputSchema(): array
553441
{
554-
return 'test';
442+
return ['message' => 'result'];
555443
}
556444
}

tests/Unit/Capability/Discovery/SchemaGeneratorTest.php

Lines changed: 2 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -335,32 +335,16 @@ public function testGenerateOutputSchemaReturnsNullForVoidReturnType(): void
335335
$this->assertNull($schema);
336336
}
337337

338-
/**
339-
* @dataProvider providesAllOutputSchemaResultReturnTypes
340-
*/
341-
public function testGenerateOutputSchemaForBasicReturnTypes(string $methodName, string $expectedType): void
342-
{
343-
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, $methodName);
344-
$schema = $this->schemaGenerator->generateOutputSchema($method);
345-
$this->assertEquals([
346-
'type' => 'object',
347-
'properties' => [
348-
'result' => ['type' => $expectedType],
349-
],
350-
'required' => ['result'],
351-
], $schema);
352-
}
353-
354338
public function testGenerateOutputSchemaWithReturnDescription(): void
355339
{
356340
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'returnWithExplicitOutputSchema');
357341
$schema = $this->schemaGenerator->generateOutputSchema($method);
358342
$this->assertEquals([
359343
'type' => 'object',
360344
'properties' => [
361-
'result' => ['type' => 'string'],
345+
'message' => ['type' => 'string'],
362346
],
363-
'required' => ['result'],
347+
'required' => ['message'],
364348
'description' => 'The result of the operation',
365349
], $schema);
366350
}
@@ -375,32 +359,6 @@ public function testGenerateOutputSchemaForArrayReturnType(): void
375359
], $schema);
376360
}
377361

378-
public function testGenerateOutputSchemaForUnionReturnType(): void
379-
{
380-
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'unionReturnWithExplicitOutputSchema');
381-
$schema = $this->schemaGenerator->generateOutputSchema($method);
382-
$this->assertEquals([
383-
'type' => 'object',
384-
'properties' => [
385-
'result' => ['type' => ['string', 'number']],
386-
],
387-
'required' => ['result'],
388-
], $schema);
389-
}
390-
391-
public function testGenerateOutputSchemaUsesPhpTypeHintOverDocBlock(): void
392-
{
393-
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'docBlockReturnType');
394-
$schema = $this->schemaGenerator->generateOutputSchema($method);
395-
$this->assertEquals([
396-
'type' => 'object',
397-
'properties' => [
398-
'result' => ['type' => 'string'],
399-
],
400-
'required' => ['result'],
401-
], $schema);
402-
}
403-
404362
public function testGenerateOutputSchemaForComplexNestedSchema(): void
405363
{
406364
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'complexNestedSchema');
@@ -410,19 +368,4 @@ public function testGenerateOutputSchemaForComplexNestedSchema(): void
410368
'additionalProperties' => true,
411369
], $schema);
412370
}
413-
414-
/**
415-
* @return array<string, array{string, string}>
416-
*/
417-
public static function providesAllOutputSchemaResultReturnTypes(): array
418-
{
419-
return [
420-
'string' => ['stringReturn', 'string'],
421-
'integer' => ['integerReturn', 'integer'],
422-
'float' => ['floatReturn', 'number'],
423-
'boolean' => ['booleanReturn', 'boolean'],
424-
'nullable string' => ['nullableReturn', 'string'],
425-
'object' => ['objectReturn', 'object'],
426-
];
427-
}
428371
}

tests/Unit/Capability/RegistryTest.php

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -536,32 +536,25 @@ public function testExtractStructuredContentReturnsNullWhenOutputSchemaIsNull():
536536
$this->assertNull($toolRef->extractStructuredContent('result'));
537537
}
538538

539-
public function testExtractStructuredContentWrapsScalarInResultKey(): void
539+
public function testExtractStructuredContentReturnsArrayMatchingSchema(): void
540540
{
541541
$tool = $this->createValidTool('test_tool', [
542542
'type' => 'object',
543543
'properties' => [
544-
'result' => ['type' => 'string'],
544+
'param' => ['type' => 'string'],
545545
],
546+
'required' => ['param'],
546547
]);
547-
$this->registry->registerTool($tool, fn () => 'hello');
548-
549-
$toolRef = $this->registry->getTool('test_tool');
550-
$this->assertEquals(['result' => 'hello'], $toolRef->extractStructuredContent('hello'));
551-
}
552-
553-
public function testExtractStructuredContentWrapsArrayInResultKey(): void
554-
{
555-
$tool = $this->createValidTool('test_tool', [
556-
'type' => 'object',
557-
'properties' => [
558-
'result' => ['type' => 'object'],
559-
],
548+
$this->registry->registerTool($tool, fn () => [
549+
'param' => 'test',
560550
]);
561-
$this->registry->registerTool($tool, fn () => ['key' => 'value']);
562551

563552
$toolRef = $this->registry->getTool('test_tool');
564-
$this->assertEquals(['result' => ['key' => 'value']], $toolRef->extractStructuredContent(['key' => 'value']));
553+
$this->assertEquals([
554+
'param' => 'test',
555+
], $toolRef->extractStructuredContent([
556+
'param' => 'test',
557+
]));
565558
}
566559

567560
public function testExtractStructuredContentReturnsArrayDirectlyForAdditionalProperties(): void
@@ -576,22 +569,6 @@ public function testExtractStructuredContentReturnsArrayDirectlyForAdditionalPro
576569
$this->assertEquals(['success' => true, 'message' => 'done'], $toolRef->extractStructuredContent(['success' => true, 'message' => 'done']));
577570
}
578571

579-
public function testExtractStructuredContentNormalizesObjectsToArrays(): void
580-
{
581-
$tool = $this->createValidTool('test_tool', [
582-
'type' => 'object',
583-
'additionalProperties' => true,
584-
]);
585-
$object = new \stdClass();
586-
$object->key = 'value';
587-
$this->registry->registerTool($tool, fn () => $object);
588-
589-
$toolRef = $this->registry->getTool('test_tool');
590-
$result = $toolRef->extractStructuredContent($object);
591-
$this->assertIsArray($result);
592-
$this->assertEquals(['key' => 'value'], $result);
593-
}
594-
595572
private function createValidTool(string $name, ?array $outputSchema = null): Tool
596573
{
597574
return new Tool(

0 commit comments

Comments
 (0)