Skip to content

Commit b9a1058

Browse files
committed
issues-68 No Ability to set outputSchema
1 parent 3dcd2f7 commit b9a1058

File tree

4 files changed

+127
-36
lines changed

4 files changed

+127
-36
lines changed

phpstan-baseline.neon

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,30 +54,6 @@ parameters:
5454
count: 1
5555
path: src/Server/Builder.php
5656

57-
-
58-
message: '#^Method Mcp\\Server\\Builder\:\:addTool\(\) has parameter \$inputSchema with no value type specified in iterable type array\.$#'
59-
identifier: missingType.iterableValue
60-
count: 1
61-
path: src/Server/Builder.php
62-
63-
-
64-
message: '#^Method Mcp\\Server\\Builder\:\:getCompletionProviders\(\) return type has no value type specified in iterable type array\.$#'
65-
identifier: missingType.iterableValue
66-
count: 1
67-
path: src/Server/Builder.php
68-
69-
-
70-
message: '#^Method Mcp\\Server\\Builder\:\:setDiscovery\(\) has parameter \$excludeDirs with no value type specified in iterable type array\.$#'
71-
identifier: missingType.iterableValue
72-
count: 1
73-
path: src/Server/Builder.php
74-
75-
-
76-
message: '#^Method Mcp\\Server\\Builder\:\:setDiscovery\(\) has parameter \$scanDirs with no value type specified in iterable type array\.$#'
77-
identifier: missingType.iterableValue
78-
count: 1
79-
path: src/Server/Builder.php
80-
8157
-
8258
message: '#^Property Mcp\\Server\\Builder\:\:\$instructions is never read, only written\.$#'
8359
identifier: property.onlyWritten
@@ -105,5 +81,5 @@ parameters:
10581
-
10682
message: '#^Property Mcp\\Server\\Builder\:\:\$tools type has no value type specified in iterable type array\.$#'
10783
identifier: missingType.iterableValue
108-
count: 1
84+
count: 3
10985
path: src/Server/Builder.php

src/Schema/Tool.php

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,46 @@
2323
* properties: array<string, mixed>,
2424
* required: string[]|null
2525
* }
26+
* @phpstan-type ToolOutputSchema array{
27+
* type: 'object',
28+
* properties: array<string, mixed>,
29+
* required: string[]|null
30+
* }
2631
* @phpstan-type ToolData array{
2732
* name: string,
2833
* inputSchema: ToolInputSchema,
2934
* description?: string|null,
3035
* annotations?: ToolAnnotationsData,
36+
* outputSchema?: ToolOutputSchema,
3137
* }
3238
*
3339
* @author Kyrian Obikwelu <[email protected]>
3440
*/
3541
class Tool implements \JsonSerializable
3642
{
3743
/**
38-
* @param string $name the name of the tool
39-
* @param string|null $description A human-readable description of the tool.
40-
* This can be used by clients to improve the LLM's understanding of
41-
* available tools. It can be thought of like a "hint" to the model.
42-
* @param ToolInputSchema $inputSchema a JSON Schema object (as a PHP array) defining the expected 'arguments' for the tool
43-
* @param ToolAnnotations|null $annotations optional additional tool information
44+
* @param string $name the name of the tool
45+
* @param string|null $description A human-readable description of the tool.
46+
* This can be used by clients to improve the LLM's understanding of
47+
* available tools. It can be thought of like a "hint" to the model.
48+
* @param ToolInputSchema $inputSchema a JSON Schema object (as a PHP array) defining the expected 'arguments' for the tool
49+
* @param ToolAnnotations|null $annotations optional additional tool information
50+
* @param ToolOutputSchema|null $outputSchema optional JSON Schema object (as a PHP array) defining the expected output structure
4451
*/
4552
public function __construct(
4653
public readonly string $name,
4754
public readonly array $inputSchema,
4855
public readonly ?string $description,
4956
public readonly ?ToolAnnotations $annotations,
57+
public readonly ?array $outputSchema = null,
5058
) {
5159
if (!isset($inputSchema['type']) || 'object' !== $inputSchema['type']) {
5260
throw new InvalidArgumentException('Tool inputSchema must be a JSON Schema of type "object".');
5361
}
62+
63+
if (null !== $outputSchema && (!isset($outputSchema['type']) || 'object' !== $outputSchema['type'])) {
64+
throw new InvalidArgumentException('Tool outputSchema must be a JSON Schema of type "object" or null.');
65+
}
5466
}
5567

5668
/**
@@ -71,11 +83,21 @@ public static function fromArray(array $data): self
7183
$data['inputSchema']['properties'] = new \stdClass();
7284
}
7385

86+
if (isset($data['outputSchema']) && \is_array($data['outputSchema'])) {
87+
if (!isset($data['outputSchema']['type']) || 'object' !== $data['outputSchema']['type']) {
88+
throw new InvalidArgumentException('Tool outputSchema must be of type "object".');
89+
}
90+
if (isset($data['outputSchema']['properties']) && \is_array($data['outputSchema']['properties']) && empty($data['outputSchema']['properties'])) {
91+
$data['outputSchema']['properties'] = new \stdClass();
92+
}
93+
}
94+
7495
return new self(
7596
$data['name'],
7697
$data['inputSchema'],
7798
isset($data['description']) && \is_string($data['description']) ? $data['description'] : null,
78-
isset($data['annotations']) && \is_array($data['annotations']) ? ToolAnnotations::fromArray($data['annotations']) : null
99+
isset($data['annotations']) && \is_array($data['annotations']) ? ToolAnnotations::fromArray($data['annotations']) : null,
100+
$data['outputSchema']
79101
);
80102
}
81103

@@ -85,6 +107,7 @@ public static function fromArray(array $data): self
85107
* inputSchema: ToolInputSchema,
86108
* description?: string,
87109
* annotations?: ToolAnnotations,
110+
* outputSchema?: ToolOutputSchema,
88111
* }
89112
*/
90113
public function jsonSerialize(): array
@@ -99,6 +122,9 @@ public function jsonSerialize(): array
99122
if (null !== $this->annotations) {
100123
$data['annotations'] = $this->annotations;
101124
}
125+
if (null !== $this->outputSchema) {
126+
$data['outputSchema'] = $this->outputSchema;
127+
}
102128

103129
return $data;
104130
}

src/Server/Builder.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ final class Builder
8181
private ?string $instructions = null;
8282

8383
/** @var array<
84-
* array{handler: array|string|\Closure,
84+
* array{handler: callable|array|string,
8585
* name: string|null,
8686
* description: string|null,
87-
* annotations: ToolAnnotations|null}
87+
* annotations: ToolAnnotations|null,
88+
* inputSchema: array|null,
89+
* outputSchema: array|null}
8890
* > */
8991
private array $tools = [];
9092

@@ -223,6 +225,10 @@ public function setSession(
223225
return $this;
224226
}
225227

228+
/**
229+
* @param array<string> $scanDirs
230+
* @param array<string> $excludeDirs
231+
*/
226232
public function setDiscovery(
227233
string $basePath,
228234
array $scanDirs = ['.', 'src'],
@@ -239,15 +245,19 @@ public function setDiscovery(
239245

240246
/**
241247
* Manually registers a tool handler.
248+
*
249+
* @param array{type: 'object', properties: array<string, mixed>, required: string[]|null}|null $inputSchema
250+
* @param array{type: 'object', properties: array<string, mixed>, required: string[]|null}|null $outputSchema
242251
*/
243252
public function addTool(
244253
callable|array|string $handler,
245254
?string $name = null,
246255
?string $description = null,
247256
?ToolAnnotations $annotations = null,
248257
?array $inputSchema = null,
258+
?array $outputSchema = null,
249259
): self {
250-
$this->tools[] = compact('handler', 'name', 'description', 'annotations', 'inputSchema');
260+
$this->tools[] = compact('handler', 'name', 'description', 'annotations', 'inputSchema', 'outputSchema');
251261

252262
return $this;
253263
}
@@ -383,8 +393,9 @@ private function registerCapabilities(
383393
}
384394

385395
$inputSchema = $data['inputSchema'] ?? $schemaGenerator->generate($reflection);
396+
$outputSchema = isset($data['outputSchema']) && \is_array($data['outputSchema']) ? $data['outputSchema'] : null;
386397

387-
$tool = new Tool($name, $inputSchema, $description, $data['annotations']);
398+
$tool = new Tool($name, $inputSchema, $description, $data['annotations'], $outputSchema);
388399
$registry->registerTool($tool, $data['handler'], true);
389400

390401
$handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (\is_array(
@@ -533,6 +544,9 @@ private function registerCapabilities(
533544
$logger->debug('Manual element registration complete.');
534545
}
535546

547+
/**
548+
* @return array<string>
549+
*/
536550
private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array
537551
{
538552
$completionProviders = [];

tests/Unit/Capability/Tool/ToolCallerTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,71 @@ public function testCallHandlesBooleanFalseResult(): void
610610
$this->assertEquals('false', $result->content[0]->text);
611611
}
612612

613+
public function testToolWithOutputSchemaIsProperlyConfigured(): void
614+
{
615+
$tool = $this->createValidTool('output_schema_tool');
616+
617+
// Verify the tool has outputSchema configured
618+
$this->assertNotNull($tool->outputSchema);
619+
$this->assertSame('object', $tool->outputSchema['type']);
620+
$this->assertArrayHasKey('properties', $tool->outputSchema);
621+
$this->assertArrayHasKey('result', $tool->outputSchema['properties']);
622+
$this->assertSame('string', $tool->outputSchema['properties']['result']['type']);
623+
$this->assertSame('The tool execution result', $tool->outputSchema['properties']['result']['description']);
624+
$this->assertArrayHasKey('required', $tool->outputSchema);
625+
$this->assertContains('result', $tool->outputSchema['required']);
626+
627+
// Verify JSON serialization includes outputSchema
628+
$json = $tool->jsonSerialize();
629+
$this->assertArrayHasKey('outputSchema', $json);
630+
$this->assertSame($tool->outputSchema, $json['outputSchema']);
631+
}
632+
633+
public function testCallHandlesStdClassResultWithOutputSchema(): void
634+
{
635+
$request = new CallToolRequest('structured_tool', []);
636+
$tool = $this->createValidTool('structured_tool');
637+
638+
// Create a structured result that matches the outputSchema
639+
$structuredResult = new \stdClass();
640+
$structuredResult->result = 'structured response';
641+
$structuredResult->metadata = 'additional info';
642+
643+
$toolReference = new ToolReference($tool, fn () => $structuredResult);
644+
645+
$this->referenceProvider
646+
->expects($this->once())
647+
->method('getTool')
648+
->with('structured_tool')
649+
->willReturn($toolReference);
650+
651+
$this->referenceHandler
652+
->expects($this->once())
653+
->method('handle')
654+
->with($toolReference, [])
655+
->willReturn($structuredResult);
656+
657+
$this->logger
658+
->expects($this->exactly(2))
659+
->method('debug');
660+
661+
$result = $this->toolCaller->call($request);
662+
663+
$this->assertInstanceOf(CallToolResult::class, $result);
664+
$this->assertCount(1, $result->content);
665+
$this->assertInstanceOf(TextContent::class, $result->content[0]);
666+
667+
// Verify the structured result is properly formatted
668+
$this->assertStringContainsString('"result": "structured response"', $result->content[0]->text);
669+
$this->assertStringContainsString('"metadata": "additional info"', $result->content[0]->text);
670+
671+
// Verify the tool has outputSchema configured
672+
$this->assertNotNull($tool->outputSchema);
673+
$this->assertSame('object', $tool->outputSchema['type']);
674+
$this->assertArrayHasKey('properties', $tool->outputSchema);
675+
$this->assertArrayHasKey('result', $tool->outputSchema['properties']);
676+
}
677+
613678
private function createValidTool(string $name): Tool
614679
{
615680
return new Tool(
@@ -623,6 +688,16 @@ private function createValidTool(string $name): Tool
623688
],
624689
description: "Test tool: {$name}",
625690
annotations: null,
691+
outputSchema: [
692+
'type' => 'object',
693+
'properties' => [
694+
'result' => [
695+
'type' => 'string',
696+
'description' => 'The tool execution result',
697+
],
698+
],
699+
'required' => ['result'],
700+
],
626701
);
627702
}
628703
}

0 commit comments

Comments
 (0)