Skip to content

Commit 4d56220

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

File tree

8 files changed

+190
-42
lines changed

8 files changed

+190
-42
lines changed

examples/05-stdio-env-variables/EnvToolHandler.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,35 @@ final class EnvToolHandler
2323
*
2424
* @return array<string, string|int> the result, varying by APP_MODE
2525
*/
26-
#[McpTool(name: 'process_data_by_mode')]
26+
#[McpTool(
27+
name: 'process_data_by_mode',
28+
outputSchema: [
29+
'type' => 'object',
30+
'properties' => [
31+
'mode' => [
32+
'type' => 'string',
33+
'description' => 'The processing mode used',
34+
],
35+
'processed_input' => [
36+
'type' => 'string',
37+
'description' => 'The processed input data (only in debug mode)',
38+
],
39+
'processed_input_length' => [
40+
'type' => 'integer',
41+
'description' => 'The length of the processed input (only in production mode)',
42+
],
43+
'original_input' => [
44+
'type' => 'string',
45+
'description' => 'The original input data (only in default mode)',
46+
],
47+
'message' => [
48+
'type' => 'string',
49+
'description' => 'A descriptive message about the processing',
50+
],
51+
],
52+
'required' => ['mode', 'message'],
53+
]
54+
)]
2755
public function processData(string $input): array
2856
{
2957
$appMode = getenv('APP_MODE'); // Read from environment

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/Capability/Attribute/McpTool.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@
2020
class McpTool
2121
{
2222
/**
23-
* @param string|null $name The name of the tool (defaults to the method name)
24-
* @param string|null $description The description of the tool (defaults to the DocBlock/inferred)
25-
* @param ToolAnnotations|null $annotations Optional annotations describing tool behavior
23+
* @param string|null $name The name of the tool (defaults to the method name)
24+
* @param string|null $description The description of the tool (defaults to the DocBlock/inferred)
25+
* @param ToolAnnotations|null $annotations Optional annotations describing tool behavior
26+
* @param array<string, mixed>|null $outputSchema Optional JSON Schema object (as a PHP array) defining the expected output structure
2627
*/
2728
public function __construct(
2829
public ?string $name = null,
2930
public ?string $description = null,
3031
public ?ToolAnnotations $annotations = null,
32+
public ?array $outputSchema = null,
3133
) {
3234
}
3335
}

src/Capability/Discovery/Discoverer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun
238238
$name = $instance->name ?? ('__invoke' === $methodName ? $classShortName : $methodName);
239239
$description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null;
240240
$inputSchema = $this->schemaGenerator->generate($method);
241-
$tool = new Tool($name, $inputSchema, $description, $instance->annotations);
241+
$tool = new Tool($name, $inputSchema, $description, $instance->annotations, $instance->outputSchema);
242242
$tools[$name] = new ToolReference($tool, [$className, $methodName], false);
243243
++$discoveredCount['tools'];
244244
break;

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/Attribute/McpToolTest.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ public function testInstantiatesWithCorrectProperties(): void
3333
public function testInstantiatesWithNullValuesForNameAndDescription(): void
3434
{
3535
// Arrange & Act
36-
$attribute = new McpTool(name: null, description: null);
36+
$attribute = new McpTool(name: null, description: null, outputSchema: null);
3737

3838
// Assert
3939
$this->assertNull($attribute->name);
4040
$this->assertNull($attribute->description);
41+
$this->assertNull($attribute->outputSchema);
4142
}
4243

4344
public function testInstantiatesWithMissingOptionalArguments(): void
@@ -48,5 +49,31 @@ public function testInstantiatesWithMissingOptionalArguments(): void
4849
// Assert
4950
$this->assertNull($attribute->name);
5051
$this->assertNull($attribute->description);
52+
$this->assertNull($attribute->outputSchema);
53+
}
54+
55+
public function testInstantiatesWithOutputSchema(): void
56+
{
57+
// Arrange
58+
$name = 'test-tool-name';
59+
$description = 'This is a test description.';
60+
$outputSchema = [
61+
'type' => 'object',
62+
'properties' => [
63+
'result' => [
64+
'type' => 'string',
65+
'description' => 'The result of the operation',
66+
],
67+
],
68+
'required' => ['result'],
69+
];
70+
71+
// Act
72+
$attribute = new McpTool(name: $name, description: $description, outputSchema: $outputSchema);
73+
74+
// Assert
75+
$this->assertSame($name, $attribute->name);
76+
$this->assertSame($description, $attribute->description);
77+
$this->assertSame($outputSchema, $attribute->outputSchema);
5178
}
5279
}

0 commit comments

Comments
 (0)