|
11 | 11 |
|
12 | 12 | namespace Mcp\Capability\Discovery; |
13 | 13 |
|
| 14 | +use Mcp\Capability\Attribute\McpTool; |
14 | 15 | use Mcp\Capability\Attribute\Schema; |
15 | 16 | use Mcp\Server\ClientGateway; |
16 | 17 | use phpDocumentor\Reflection\DocBlock\Tags\Param; |
@@ -80,6 +81,47 @@ public function generate(\ReflectionMethod|\ReflectionFunction $reflection): arr |
80 | 81 | return $this->buildSchemaFromParameters($parametersInfo, $methodSchema); |
81 | 82 | } |
82 | 83 |
|
| 84 | + /** |
| 85 | + * Generates a JSON Schema object (as a PHP array) for a method's or function's return type. |
| 86 | + * |
| 87 | + * Checks for explicit outputSchema in McpTool attribute first, then auto-generates from return type. |
| 88 | + * |
| 89 | + * @return array<string, mixed>|null |
| 90 | + */ |
| 91 | + public function generateOutputSchema(\ReflectionMethod|\ReflectionFunction $reflection): ?array |
| 92 | + { |
| 93 | + // Check if McpTool attribute has explicit outputSchema |
| 94 | + $mcpToolAttrs = $reflection->getAttributes(McpTool::class, \ReflectionAttribute::IS_INSTANCEOF); |
| 95 | + if (!empty($mcpToolAttrs)) { |
| 96 | + $mcpToolInstance = $mcpToolAttrs[0]->newInstance(); |
| 97 | + if (null !== $mcpToolInstance->outputSchema) { |
| 98 | + return $mcpToolInstance->outputSchema; |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + $docComment = $reflection->getDocComment() ?: null; |
| 103 | + $docBlock = $this->docBlockParser->parseDocBlock($docComment); |
| 104 | + |
| 105 | + $docBlockReturnType = $this->docBlockParser->getReturnTypeString($docBlock); |
| 106 | + $returnDescription = $this->docBlockParser->getReturnDescription($docBlock); |
| 107 | + |
| 108 | + $reflectionReturnType = $reflection->getReturnType(); |
| 109 | + $reflectionReturnTypeString = $reflectionReturnType |
| 110 | + ? $this->getTypeStringFromReflection($reflectionReturnType, $reflectionReturnType->allowsNull()) |
| 111 | + : null; |
| 112 | + |
| 113 | + // Use DocBlock with generics, otherwise reflection, otherwise DocBlock |
| 114 | + $returnTypeString = ($docBlockReturnType && str_contains($docBlockReturnType, '<')) |
| 115 | + ? $docBlockReturnType |
| 116 | + : ($reflectionReturnTypeString ?: $docBlockReturnType); |
| 117 | + |
| 118 | + if (!$returnTypeString || 'void' === strtolower($returnTypeString)) { |
| 119 | + return null; |
| 120 | + } |
| 121 | + |
| 122 | + return $this->buildOutputSchemaFromType($returnTypeString, $returnDescription); |
| 123 | + } |
| 124 | + |
83 | 125 | /** |
84 | 126 | * Extracts method-level or function-level Schema attribute. |
85 | 127 | * |
@@ -794,4 +836,42 @@ private function mapSimpleTypeToJsonSchema(string $type): string |
794 | 836 | default => \in_array(strtolower($type), ['datetime', 'datetimeinterface']) ? 'string' : 'object', |
795 | 837 | }; |
796 | 838 | } |
| 839 | + |
| 840 | + /** |
| 841 | + * Builds an output schema from a return type string. |
| 842 | + * |
| 843 | + * @return array<string, mixed> |
| 844 | + */ |
| 845 | + private function buildOutputSchemaFromType(string $returnTypeString, ?string $description): array |
| 846 | + { |
| 847 | + // Handle array types - treat as object with additionalProperties |
| 848 | + if (str_contains($returnTypeString, 'array')) { |
| 849 | + $schema = [ |
| 850 | + 'type' => 'object', |
| 851 | + 'additionalProperties' => true, |
| 852 | + ]; |
| 853 | + } else { |
| 854 | + // Use mapPhpTypeToJsonSchemaType to handle union types and nullable types |
| 855 | + $mappedTypes = $this->mapPhpTypeToJsonSchemaType($returnTypeString); |
| 856 | + |
| 857 | + $nonNullTypes = array_filter($mappedTypes, fn ($type) => 'null' !== $type); |
| 858 | + |
| 859 | + // If it's a union type use the array directly, or use the first (and only) type |
| 860 | + $typeSchema = \count($nonNullTypes) > 1 ? array_values($nonNullTypes) : ($nonNullTypes[0] ?? 'object'); |
| 861 | + |
| 862 | + $schema = [ |
| 863 | + 'type' => 'object', |
| 864 | + 'properties' => [ |
| 865 | + 'result' => ['type' => $typeSchema], |
| 866 | + ], |
| 867 | + 'required' => ['result'], |
| 868 | + ]; |
| 869 | + } |
| 870 | + |
| 871 | + if ($description) { |
| 872 | + $schema['description'] = $description; |
| 873 | + } |
| 874 | + |
| 875 | + return $schema; |
| 876 | + } |
797 | 877 | } |
0 commit comments