-
Notifications
You must be signed in to change notification settings - Fork 81
[Server] issues-68 No Ability to set outputSchema #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -136,4 +136,51 @@ public function getParamTypeString(?Param $paramTag): ?string | |||||
|
|
||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Gets the return type string from a Return tag. | ||||||
| */ | ||||||
| public function getReturnTypeString(?DocBlock $docBlock): ?string | ||||||
| { | ||||||
| if (!$docBlock) { | ||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| $returnTags = $docBlock->getTagsByName('return'); | ||||||
| if (empty($returnTags)) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and try to avoid empty:
Suggested change
|
||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| $returnTag = $returnTags[0]; | ||||||
| if (method_exists($returnTag, 'getType') && $returnTag->getType()) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we check for
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and should be inverted to: if (!$returnTag instanceof TagWithType) {
return null;
}it's better to continue with the style of early exits - like you did in the beginning of the method |
||||||
| $typeFromTag = trim((string) $returnTag->getType()); | ||||||
| if (!empty($typeFromTag)) { | ||||||
| return ltrim($typeFromTag, '\\'); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Gets the return type description from a Return tag. | ||||||
| */ | ||||||
| public function getReturnDescription(?DocBlock $docBlock): ?string | ||||||
| { | ||||||
| if (!$docBlock) { | ||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| $returnTags = $docBlock->getTagsByName('return'); | ||||||
| if (empty($returnTags)) { | ||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| $returnTag = $returnTags[0]; | ||||||
| $description = method_exists($returnTag, 'getDescription') | ||||||
| ? trim((string) $returnTag->getDescription()) | ||||||
| : ''; | ||||||
|
|
||||||
| return $description ?: null; | ||||||
| } | ||||||
|
Comment on lines
+168
to
+185
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similar comments would apply to this method like with |
||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,6 +79,36 @@ public function generate(\ReflectionMethod|\ReflectionFunction $reflection): arr | |
| return $this->buildSchemaFromParameters($parametersInfo, $methodSchema); | ||
| } | ||
|
|
||
| /** | ||
| * Generates a JSON Schema object (as a PHP array) for a method's or function's return type. | ||
| * | ||
| * @return array<string, mixed>|null | ||
| */ | ||
| public function generateOutputSchema(\ReflectionMethod|\ReflectionFunction $reflection): ?array | ||
| { | ||
| $docComment = $reflection->getDocComment() ?: null; | ||
| $docBlock = $this->docBlockParser->parseDocBlock($docComment); | ||
|
|
||
| $docBlockReturnType = $this->docBlockParser->getReturnTypeString($docBlock); | ||
| $returnDescription = $this->docBlockParser->getReturnDescription($docBlock); | ||
|
|
||
| $reflectionReturnType = $reflection->getReturnType(); | ||
| $reflectionReturnTypeString = $reflectionReturnType | ||
| ? $this->getTypeStringFromReflection($reflectionReturnType, $reflectionReturnType->allowsNull()) | ||
| : null; | ||
|
|
||
| // Use DocBlock with generics, otherwise reflection, otherwise DocBlock | ||
| $returnTypeString = ($docBlockReturnType && str_contains($docBlockReturnType, '<')) | ||
| ? $docBlockReturnType | ||
| : ($reflectionReturnTypeString ?: $docBlockReturnType); | ||
|
|
||
| if (!$returnTypeString || 'void' === strtolower($returnTypeString)) { | ||
| return null; | ||
| } | ||
|
|
||
| return $this->buildOutputSchemaFromType($returnTypeString, $returnDescription); | ||
| } | ||
|
|
||
|
Comment on lines
+82
to
+111
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please add tests for this method |
||
| /** | ||
| * Extracts method-level or function-level Schema attribute. | ||
| * | ||
|
|
@@ -784,4 +814,36 @@ private function mapSimpleTypeToJsonSchema(string $type): string | |
| default => \in_array(strtolower($type), ['datetime', 'datetimeinterface']) ? 'string' : 'object', | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Builds an output schema from a return type string. | ||
| * | ||
| * @return array<string, mixed> | ||
| */ | ||
| private function buildOutputSchemaFromType(string $returnTypeString, ?string $description): array | ||
| { | ||
| // Handle array types - treat as object with additionalProperties | ||
| if (str_contains($returnTypeString, 'array')) { | ||
| $schema = [ | ||
| 'type' => 'object', | ||
| 'additionalProperties' => ['type' => 'mixed'], | ||
| ]; | ||
| } else { | ||
| // Handle other types - wrap in object for MCP compatibility | ||
| $mappedType = $this->mapSimpleTypeToJsonSchema($returnTypeString); | ||
| $schema = [ | ||
| 'type' => 'object', | ||
| 'properties' => [ | ||
| 'result' => ['type' => $mappedType], | ||
| ], | ||
| 'required' => ['result'], | ||
| ]; | ||
| } | ||
|
|
||
| if ($description) { | ||
| $schema['description'] = $description; | ||
| } | ||
|
|
||
| return $schema; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,34 +23,46 @@ | |
| * properties: array<string, mixed>, | ||
| * required: string[]|null | ||
| * } | ||
| * @phpstan-type ToolOutputSchema array{ | ||
| * type: 'object', | ||
| * properties: array<string, mixed>, | ||
| * required: string[]|null | ||
| * } | ||
| * @phpstan-type ToolData array{ | ||
| * name: string, | ||
| * inputSchema: ToolInputSchema, | ||
| * description?: string|null, | ||
| * annotations?: ToolAnnotationsData, | ||
| * outputSchema?: ToolOutputSchema, | ||
| * } | ||
| * | ||
| * @author Kyrian Obikwelu <[email protected]> | ||
| */ | ||
| class Tool implements \JsonSerializable | ||
| { | ||
| /** | ||
| * @param string $name the name of the tool | ||
| * @param string|null $description A human-readable description of the tool. | ||
| * This can be used by clients to improve the LLM's understanding of | ||
| * available tools. It can be thought of like a "hint" to the model. | ||
| * @param ToolInputSchema $inputSchema a JSON Schema object (as a PHP array) defining the expected 'arguments' for the tool | ||
| * @param ToolAnnotations|null $annotations optional additional tool information | ||
| * @param string $name the name of the tool | ||
| * @param string|null $description A human-readable description of the tool. | ||
| * This can be used by clients to improve the LLM's understanding of | ||
| * available tools. It can be thought of like a "hint" to the model. | ||
| * @param ToolInputSchema $inputSchema a JSON Schema object (as a PHP array) defining the expected 'arguments' for the tool | ||
| * @param ToolAnnotations|null $annotations optional additional tool information | ||
| * @param ToolOutputSchema|null $outputSchema optional JSON Schema object (as a PHP array) defining the expected output structure | ||
| */ | ||
| public function __construct( | ||
| public readonly string $name, | ||
| public readonly array $inputSchema, | ||
| public readonly ?string $description, | ||
| public readonly ?ToolAnnotations $annotations, | ||
| public readonly ?array $outputSchema = null, | ||
| ) { | ||
| if (!isset($inputSchema['type']) || 'object' !== $inputSchema['type']) { | ||
| throw new InvalidArgumentException('Tool inputSchema must be a JSON Schema of type "object".'); | ||
| } | ||
|
|
||
| if (null !== $outputSchema && (!isset($outputSchema['type']) || 'object' !== $outputSchema['type'])) { | ||
| throw new InvalidArgumentException('Tool outputSchema must be a JSON Schema of type "object" or null.'); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -71,11 +83,21 @@ public static function fromArray(array $data): self | |
| $data['inputSchema']['properties'] = new \stdClass(); | ||
| } | ||
|
|
||
| if (isset($data['outputSchema']) && \is_array($data['outputSchema'])) { | ||
| if (!isset($data['outputSchema']['type']) || 'object' !== $data['outputSchema']['type']) { | ||
| throw new InvalidArgumentException('Tool outputSchema must be of type "object".'); | ||
| } | ||
| if (isset($data['outputSchema']['properties']) && \is_array($data['outputSchema']['properties']) && empty($data['outputSchema']['properties'])) { | ||
| $data['outputSchema']['properties'] = new \stdClass(); | ||
| } | ||
| } | ||
|
|
||
| return new self( | ||
| $data['name'], | ||
| $data['inputSchema'], | ||
| isset($data['description']) && \is_string($data['description']) ? $data['description'] : null, | ||
| isset($data['annotations']) && \is_array($data['annotations']) ? ToolAnnotations::fromArray($data['annotations']) : null | ||
| isset($data['annotations']) && \is_array($data['annotations']) ? ToolAnnotations::fromArray($data['annotations']) : null, | ||
| $data['outputSchema'] | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -85,6 +107,7 @@ public static function fromArray(array $data): self | |
| * inputSchema: ToolInputSchema, | ||
| * description?: string, | ||
| * annotations?: ToolAnnotations, | ||
| * outputSchema?: ToolOutputSchema, | ||
| * } | ||
| */ | ||
| public function jsonSerialize(): array | ||
|
|
@@ -99,6 +122,9 @@ public function jsonSerialize(): array | |
| if (null !== $this->annotations) { | ||
| $data['annotations'] = $this->annotations; | ||
| } | ||
| if (null !== $this->outputSchema) { | ||
| $data['outputSchema'] = $this->outputSchema; | ||
| } | ||
|
|
||
| return $data; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
always like to be explicit