diff --git a/examples/env-variables/EnvToolHandler.php b/examples/env-variables/EnvToolHandler.php index f7cad817..bab055e9 100644 --- a/examples/env-variables/EnvToolHandler.php +++ b/examples/env-variables/EnvToolHandler.php @@ -23,7 +23,31 @@ final class EnvToolHandler * * @return array the result, varying by APP_MODE */ - #[McpTool(name: 'process_data_by_mode')] + #[McpTool( + name: 'process_data_by_mode', + outputSchema: [ + 'type' => 'object', + 'properties' => [ + 'mode' => [ + 'type' => 'string', + 'description' => 'The processing mode used', + ], + 'processed_input' => [ + 'type' => 'string', + 'description' => 'The processed input data', + ], + 'original_input' => [ + 'type' => 'string', + 'description' => 'The original input data (only in default mode)', + ], + 'message' => [ + 'type' => 'string', + 'description' => 'A descriptive message about the processing', + ], + ], + 'required' => ['mode', 'message'], + ] + )] public function processData(string $input): array { $appMode = getenv('APP_MODE'); // Read from environment diff --git a/src/Capability/Attribute/McpTool.php b/src/Capability/Attribute/McpTool.php index 85dbc225..fe754e90 100644 --- a/src/Capability/Attribute/McpTool.php +++ b/src/Capability/Attribute/McpTool.php @@ -21,11 +21,12 @@ class McpTool { /** - * @param string|null $name The name of the tool (defaults to the method name) - * @param string|null $description The description of the tool (defaults to the DocBlock/inferred) - * @param ToolAnnotations|null $annotations Optional annotations describing tool behavior - * @param ?Icon[] $icons Optional list of icon URLs representing the tool - * @param ?array $meta Optional metadata + * @param string|null $name The name of the tool (defaults to the method name) + * @param string|null $description The description of the tool (defaults to the DocBlock/inferred) + * @param ToolAnnotations|null $annotations Optional annotations describing tool behavior + * @param ?Icon[] $icons Optional list of icon URLs representing the tool + * @param ?array $meta Optional metadata + * @param array $outputSchema Optional JSON Schema object for defining the expected output structure */ public function __construct( public ?string $name = null, @@ -33,6 +34,7 @@ public function __construct( public ?ToolAnnotations $annotations = null, public ?array $icons = null, public ?array $meta = null, + public ?array $outputSchema = null, ) { } } diff --git a/src/Capability/Discovery/Discoverer.php b/src/Capability/Discovery/Discoverer.php index 88ec4117..f30d5fff 100644 --- a/src/Capability/Discovery/Discoverer.php +++ b/src/Capability/Discovery/Discoverer.php @@ -222,6 +222,7 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun $name = $instance->name ?? ('__invoke' === $methodName ? $classShortName : $methodName); $description = $instance->description ?? $this->docBlockParser->getSummary($docBlock) ?? null; $inputSchema = $this->schemaGenerator->generate($method); + $outputSchema = $this->schemaGenerator->generateOutputSchema($method); $tool = new Tool( $name, $inputSchema, @@ -229,6 +230,7 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun $instance->annotations, $instance->icons, $instance->meta, + $outputSchema, ); $tools[$name] = new ToolReference($tool, [$className, $methodName], false); ++$discoveredCount['tools']; diff --git a/src/Capability/Discovery/DocBlockParser.php b/src/Capability/Discovery/DocBlockParser.php index 91f417f2..47a5b441 100644 --- a/src/Capability/Discovery/DocBlockParser.php +++ b/src/Capability/Discovery/DocBlockParser.php @@ -13,6 +13,7 @@ use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock\Tags\Param; +use phpDocumentor\Reflection\DocBlock\Tags\TagWithType; use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactoryInterface; use Psr\Log\LoggerInterface; @@ -136,4 +137,53 @@ public function getParamTypeString(?Param $paramTag): ?string return null; } + + /** + * Gets the return type string from a Return tag. + */ + public function getReturnTypeString(?DocBlock $docBlock): ?string + { + if (null === $docBlock) { + return null; + } + + $returnTags = $docBlock->getTagsByName('return'); + if ([] === $returnTags) { + return null; + } + + $returnTag = $returnTags[0]; + if (!$returnTag instanceof TagWithType) { + return null; + } + + $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 (null === $docBlock) { + return null; + } + + $returnTags = $docBlock->getTagsByName('return'); + if ([] === $returnTags) { + return null; + } + + $returnTag = $returnTags[0]; + if (!$returnTag instanceof TagWithType) { + return null; + } + + return trim((string) $returnTag->getDescription()) ?: null; + } } diff --git a/src/Capability/Discovery/SchemaGenerator.php b/src/Capability/Discovery/SchemaGenerator.php index 2557f559..6f93d7c9 100644 --- a/src/Capability/Discovery/SchemaGenerator.php +++ b/src/Capability/Discovery/SchemaGenerator.php @@ -11,6 +11,7 @@ namespace Mcp\Capability\Discovery; +use Mcp\Capability\Attribute\McpTool; use Mcp\Capability\Attribute\Schema; use Mcp\Server\ClientGateway; use phpDocumentor\Reflection\DocBlock\Tags\Param; @@ -80,6 +81,47 @@ 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. + * + * Checks for explicit outputSchema in McpTool attribute first, then auto-generates from return type. + * + * @return array|null + */ + public function generateOutputSchema(\ReflectionMethod|\ReflectionFunction $reflection): ?array + { + // Check if McpTool attribute has explicit outputSchema + $mcpToolAttrs = $reflection->getAttributes(McpTool::class, \ReflectionAttribute::IS_INSTANCEOF); + if (!empty($mcpToolAttrs)) { + $mcpToolInstance = $mcpToolAttrs[0]->newInstance(); + if (null !== $mcpToolInstance->outputSchema) { + return $mcpToolInstance->outputSchema; + } + } + + $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); + } + /** * Extracts method-level or function-level Schema attribute. * @@ -794,4 +836,42 @@ 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 + */ + 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' => true, + ]; + } else { + // Use mapPhpTypeToJsonSchemaType to handle union types and nullable types + $mappedTypes = $this->mapPhpTypeToJsonSchemaType($returnTypeString); + + $nonNullTypes = array_filter($mappedTypes, fn ($type) => 'null' !== $type); + + // If it's a union type use the array directly, or use the first (and only) type + $typeSchema = \count($nonNullTypes) > 1 ? array_values($nonNullTypes) : ($nonNullTypes[0] ?? 'object'); + + $schema = [ + 'type' => 'object', + 'properties' => [ + 'result' => ['type' => $typeSchema], + ], + 'required' => ['result'], + ]; + } + + if ($description) { + $schema['description'] = $description; + } + + return $schema; + } } diff --git a/src/Capability/Registry/ToolReference.php b/src/Capability/Registry/ToolReference.php index e5b5a7df..b4594161 100644 --- a/src/Capability/Registry/ToolReference.php +++ b/src/Capability/Registry/ToolReference.php @@ -111,4 +111,66 @@ public function formatResult(mixed $toolExecutionResult): array return [new TextContent($jsonResult)]; } + + /** + * Extracts structured content from a tool result using the output schema. + * + * @param mixed $toolExecutionResult the raw value returned by the tool's PHP method + * + * @return array|null the structured content, or null if not extractable + */ + public function extractStructuredContent(mixed $toolExecutionResult): ?array + { + $outputSchema = $this->tool->outputSchema; + if (null === $outputSchema) { + return null; + } + + // If outputSchema has properties.result, wrap in result key + if (isset($outputSchema['properties']['result'])) { + return ['result' => $this->normalizeValue($toolExecutionResult)]; + } + + if (isset($outputSchema['additionalProperties'])) { + if (\is_array($toolExecutionResult)) { + // Check if it's a numeric array (list) + if (array_is_list($toolExecutionResult)) { + // Wrap list in "object" schema + return ['items' => $toolExecutionResult]; + } + + return $toolExecutionResult; + } + + if (\is_object($toolExecutionResult) && !($toolExecutionResult instanceof Content)) { + return $this->normalizeValue($toolExecutionResult); + } + } + + if (isset($outputSchema['properties'])) { + if (\is_array($toolExecutionResult)) { + return $toolExecutionResult; + } + + if (\is_object($toolExecutionResult) && !($toolExecutionResult instanceof Content)) { + return $this->normalizeValue($toolExecutionResult); + } + } + + return null; + } + + /** + * Convert objects to arrays for a normalized structured content. + * + * @throws \JsonException if JSON encoding fails for non-Content array/object results + */ + private function normalizeValue(mixed $value): mixed + { + if (\is_object($value) && !($value instanceof Content)) { + return json_decode(json_encode($value, \JSON_THROW_ON_ERROR), true, 512, \JSON_THROW_ON_ERROR); + } + + return $value; + } } diff --git a/src/Schema/Result/CallToolResult.php b/src/Schema/Result/CallToolResult.php index 4f31e034..3da016dc 100644 --- a/src/Schema/Result/CallToolResult.php +++ b/src/Schema/Result/CallToolResult.php @@ -59,12 +59,13 @@ public function __construct( /** * Create a new CallToolResult with success status. * - * @param Content[] $content The content of the tool result - * @param array|null $meta Optional metadata + * @param Content[] $content The content of the tool result + * @param array|null $meta Optional metadata + * @param array|null $structuredContent Optional structured content matching the tool's outputSchema */ - public static function success(array $content, ?array $meta = null): self + public static function success(array $content, ?array $meta = null, ?array $structuredContent = null): self { - return new self($content, false, null, $meta); + return new self($content, false, $meta, $structuredContent); } /** @@ -83,6 +84,7 @@ public static function error(array $content, ?array $meta = null): self * content: array, * isError?: bool, * _meta?: array, + * structuredContent?: array * } $data */ public static function fromArray(array $data): self diff --git a/src/Schema/Tool.php b/src/Schema/Tool.php index 3a4e8193..4a1768ee 100644 --- a/src/Schema/Tool.php +++ b/src/Schema/Tool.php @@ -24,13 +24,21 @@ * properties: array, * required: string[]|null * } + * @phpstan-type ToolOutputSchema array{ + * type: 'object', + * properties?: array, + * required?: string[]|null, + * additionalProperties?: bool|array, + * description?: string + * } * @phpstan-type ToolData array{ * name: string, * inputSchema: ToolInputSchema, * description?: string|null, * annotations?: ToolAnnotationsData, * icons?: IconData[], - * _meta?: array + * _meta?: array, + * outputSchema?: ToolOutputSchema * } * * @author Kyrian Obikwelu @@ -38,14 +46,15 @@ class Tool implements \JsonSerializable { /** - * @param string $name the name of the tool - * @param ?string $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 $annotations optional additional tool information - * @param ?Icon[] $icons optional icons representing the tool - * @param ?array $meta Optional metadata + * @param string $name the name of the tool + * @param ?string $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 $annotations optional additional tool information + * @param ?Icon[] $icons optional icons representing the tool + * @param ?array $meta Optional metadata + * @param ToolOutputSchema|null $outputSchema optional JSON Schema object (as a PHP array) defining the expected output structure */ public function __construct( public readonly string $name, @@ -54,6 +63,7 @@ public function __construct( public readonly ?ToolAnnotations $annotations, public readonly ?array $icons = null, public readonly ?array $meta = null, + 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".'); @@ -78,13 +88,23 @@ 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['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null, - isset($data['_meta']) && \is_array($data['_meta']) ? $data['_meta'] : null + isset($data['_meta']) && \is_array($data['_meta']) ? $data['_meta'] : null, + isset($data['outputSchema']) && \is_array($data['outputSchema']) ? $data['outputSchema'] : null, ); } @@ -95,7 +115,8 @@ public static function fromArray(array $data): self * description?: string, * annotations?: ToolAnnotations, * icons?: Icon[], - * _meta?: array + * _meta?: array, + * outputSchema?: ToolOutputSchema * } */ public function jsonSerialize(): array @@ -116,6 +137,9 @@ public function jsonSerialize(): array if (null !== $this->meta) { $data['_meta'] = $this->meta; } + if (null !== $this->outputSchema) { + $data['outputSchema'] = $this->outputSchema; + } return $data; } diff --git a/src/Server/Builder.php b/src/Server/Builder.php index 1d1f8d07..4b411645 100644 --- a/src/Server/Builder.php +++ b/src/Server/Builder.php @@ -87,7 +87,8 @@ final class Builder * description: ?string, * annotations: ?ToolAnnotations, * icons: ?Icon[], - * meta: ?array + * meta: ?array, + * output: ?array, * }[] */ private array $tools = []; @@ -330,6 +331,7 @@ public function setProtocolVersion(ProtocolVersion $protocolVersion): self * @param array|null $inputSchema * @param ?Icon[] $icons * @param array|null $meta + * @param array|null $outputSchema */ public function addTool( callable|array|string $handler, @@ -339,6 +341,7 @@ public function addTool( ?array $inputSchema = null, ?array $icons = null, ?array $meta = null, + ?array $outputSchema = null, ): self { $this->tools[] = compact( 'handler', @@ -348,6 +351,7 @@ public function addTool( 'inputSchema', 'icons', 'meta', + 'outputSchema', ); return $this; diff --git a/src/Server/Handler/Request/CallToolHandler.php b/src/Server/Handler/Request/CallToolHandler.php index e0430802..1b01518a 100644 --- a/src/Server/Handler/Request/CallToolHandler.php +++ b/src/Server/Handler/Request/CallToolHandler.php @@ -62,15 +62,22 @@ public function handle(Request $request, SessionInterface $session): Response|Er $arguments['_session'] = $session; - $result = $this->referenceHandler->handle($reference, $arguments); + $rawResult = $this->referenceHandler->handle($reference, $arguments); - if (!$result instanceof CallToolResult) { - $result = new CallToolResult($reference->formatResult($result)); + $structuredContent = null; + if (null !== $reference->tool->outputSchema && !$rawResult instanceof CallToolResult) { + $structuredContent = $reference->extractStructuredContent($rawResult); + } + + $result = $rawResult; + if (!$rawResult instanceof CallToolResult) { + $result = new CallToolResult($reference->formatResult($rawResult), structuredContent: $structuredContent); } $this->logger->debug('Tool executed successfully', [ 'name' => $toolName, 'result_type' => \gettype($result), + 'structured_content' => $structuredContent, ]); return new Response($request->getId(), $result); diff --git a/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-tools_call-discovered_status_check.json b/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-tools_call-discovered_status_check.json index d849f400..15424593 100644 --- a/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-tools_call-discovered_status_check.json +++ b/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-tools_call-discovered_status_check.json @@ -5,5 +5,8 @@ "text": "System status: OK (discovered)" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": "System status: OK (discovered)" + } } diff --git a/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-tools_list.json b/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-tools_list.json index 04d8ea1c..405e4b6b 100644 --- a/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-tools_list.json +++ b/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-tools_list.json @@ -22,6 +22,18 @@ "inputSchema": { "type": "object", "properties": {} + }, + "outputSchema": { + "type": "object", + "properties": { + "result": { + "type": "string" + } + }, + "required": [ + "result" + ], + "description": "a status message" } } ] diff --git a/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_all_day_reminder.json b/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_all_day_reminder.json index 1e7667fa..cafbff6c 100644 --- a/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_all_day_reminder.json +++ b/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_all_day_reminder.json @@ -5,5 +5,18 @@ "text": "{\n \"success\": true,\n \"message\": \"Event \\\"Project Deadline\\\" scheduled successfully for \\\"2024-12-15\\\".\",\n \"event_details\": {\n \"title\": \"Project Deadline\",\n \"date\": \"2024-12-15\",\n \"type\": \"reminder\",\n \"time\": \"All day\",\n \"priority\": \"Normal\",\n \"attendees\": [],\n \"invites_will_be_sent\": false\n }\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "success": true, + "message": "Event \"Project Deadline\" scheduled successfully for \"2024-12-15\".", + "event_details": { + "title": "Project Deadline", + "date": "2024-12-15", + "type": "reminder", + "time": "All day", + "priority": "Normal", + "attendees": [], + "invites_will_be_sent": false + } + } } diff --git a/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_high_priority.json b/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_high_priority.json index 5309d2e9..2b336381 100644 --- a/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_high_priority.json +++ b/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_high_priority.json @@ -5,5 +5,20 @@ "text": "{\n \"success\": true,\n \"message\": \"Event \\\"Client Call\\\" scheduled successfully for \\\"2024-12-02\\\".\",\n \"event_details\": {\n \"title\": \"Client Call\",\n \"date\": \"2024-12-02\",\n \"type\": \"call\",\n \"time\": \"14:30\",\n \"priority\": \"Normal\",\n \"attendees\": [\n \"client@example.com\"\n ],\n \"invites_will_be_sent\": false\n }\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "success": true, + "message": "Event \"Client Call\" scheduled successfully for \"2024-12-02\".", + "event_details": { + "title": "Client Call", + "date": "2024-12-02", + "type": "call", + "time": "14:30", + "priority": "Normal", + "attendees": [ + "client@example.com" + ], + "invites_will_be_sent": false + } + } } diff --git a/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_low_priority.json b/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_low_priority.json index a9f4d35f..5f8129b8 100644 --- a/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_low_priority.json +++ b/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_low_priority.json @@ -5,5 +5,20 @@ "text": "{\n \"success\": true,\n \"message\": \"Event \\\"Office Party\\\" scheduled successfully for \\\"2024-12-20\\\".\",\n \"event_details\": {\n \"title\": \"Office Party\",\n \"date\": \"2024-12-20\",\n \"type\": \"other\",\n \"time\": \"18:00\",\n \"priority\": \"Normal\",\n \"attendees\": [\n \"team@company.com\"\n ],\n \"invites_will_be_sent\": true\n }\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "success": true, + "message": "Event \"Office Party\" scheduled successfully for \"2024-12-20\".", + "event_details": { + "title": "Office Party", + "date": "2024-12-20", + "type": "other", + "time": "18:00", + "priority": "Normal", + "attendees": [ + "team@company.com" + ], + "invites_will_be_sent": true + } + } } diff --git a/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_meeting_with_time.json b/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_meeting_with_time.json index 68c6f014..b3a64379 100644 --- a/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_meeting_with_time.json +++ b/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_call-schedule_event_meeting_with_time.json @@ -5,5 +5,21 @@ "text": "{\n \"success\": true,\n \"message\": \"Event \\\"Team Standup\\\" scheduled successfully for \\\"2024-12-01\\\".\",\n \"event_details\": {\n \"title\": \"Team Standup\",\n \"date\": \"2024-12-01\",\n \"type\": \"meeting\",\n \"time\": \"09:00\",\n \"priority\": \"Normal\",\n \"attendees\": [\n \"alice@example.com\",\n \"bob@example.com\"\n ],\n \"invites_will_be_sent\": true\n }\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "success": true, + "message": "Event \"Team Standup\" scheduled successfully for \"2024-12-01\".", + "event_details": { + "title": "Team Standup", + "date": "2024-12-01", + "type": "meeting", + "time": "09:00", + "priority": "Normal", + "attendees": [ + "alice@example.com", + "bob@example.com" + ], + "invites_will_be_sent": true + } + } } diff --git a/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_list.json b/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_list.json index 5f47adca..de1e25a1 100644 --- a/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_list.json +++ b/tests/Inspector/Http/snapshots/HttpComplexToolSchemaTest-tools_list.json @@ -61,6 +61,11 @@ "date", "type" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true, + "description": "confirmation of the scheduled event" } } ] diff --git a/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_call-send_welcome.json b/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_call-send_welcome.json index 95ed1898..a9a082e5 100644 --- a/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_call-send_welcome.json +++ b/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_call-send_welcome.json @@ -5,5 +5,9 @@ "text": "{\n \"success\": true,\n \"message_sent\": \"Welcome, Alice! Welcome to our platform!\"\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "success": true, + "message_sent": "Welcome, Alice! Welcome to our platform!" + } } diff --git a/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_call-test_tool_without_params.json b/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_call-test_tool_without_params.json index cac9850a..05256164 100644 --- a/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_call-test_tool_without_params.json +++ b/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_call-test_tool_without_params.json @@ -5,5 +5,9 @@ "text": "{\n \"success\": true,\n \"message\": \"Test tool without params\"\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "success": true, + "message": "Test tool without params" + } } diff --git a/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_list.json b/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_list.json index f515430e..ec100ad4 100644 --- a/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_list.json +++ b/tests/Inspector/Http/snapshots/HttpDiscoveryUserProfileTest-tools_list.json @@ -45,6 +45,11 @@ "required": [ "userId" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true, + "description": "status of the operation" } }, { @@ -52,6 +57,10 @@ "inputSchema": { "type": "object", "properties": {} + }, + "outputSchema": { + "type": "object", + "additionalProperties": true } } ] diff --git a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-calculate_range.json b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-calculate_range.json index 817d33d9..612afc7a 100644 --- a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-calculate_range.json +++ b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-calculate_range.json @@ -5,5 +5,11 @@ "text": "{\n \"result\": 50,\n \"operation\": \"10 multiply 5\",\n \"precision\": 2,\n \"within_bounds\": true\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": 50, + "operation": "10 multiply 5", + "precision": 2, + "within_bounds": true + } } diff --git a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-format_text.json b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-format_text.json index eb9d89de..1203e8c5 100644 --- a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-format_text.json +++ b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-format_text.json @@ -5,5 +5,11 @@ "text": "{\n \"original\": \"Hello World Test\",\n \"formatted\": \"HELLO WORLD TEST\",\n \"length\": 16,\n \"format_applied\": \"uppercase\"\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "original": "Hello World Test", + "formatted": "HELLO WORLD TEST", + "length": 16, + "format_applied": "uppercase" + } } diff --git a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-generate_config.json b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-generate_config.json index e193e9fb..45e8bdc0 100644 --- a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-generate_config.json +++ b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-generate_config.json @@ -5,5 +5,30 @@ "text": "{\n \"success\": true,\n \"config\": {\n \"app\": {\n \"name\": \"TestApp\",\n \"env\": \"development\",\n \"debug\": true,\n \"url\": \"https://example.com\",\n \"port\": 8080\n },\n \"generated_at\": \"2025-01-01T00:00:00+00:00\",\n \"version\": \"1.0.0\",\n \"features\": {\n \"logging\": true,\n \"caching\": false,\n \"analytics\": false,\n \"rate_limiting\": false\n }\n },\n \"validation\": {\n \"app_name_valid\": true,\n \"url_valid\": true,\n \"port_in_range\": true\n }\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "success": true, + "config": { + "app": { + "name": "TestApp", + "env": "development", + "debug": true, + "url": "https://example.com", + "port": 8080 + }, + "generated_at": "2025-01-01T00:00:00+00:00", + "version": "1.0.0", + "features": { + "logging": true, + "caching": false, + "analytics": false, + "rate_limiting": false + } + }, + "validation": { + "app_name_valid": true, + "url_valid": true, + "port_in_range": true + } + } } diff --git a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-manage_list.json b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-manage_list.json index 25623f28..42c11384 100644 --- a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-manage_list.json +++ b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-manage_list.json @@ -5,5 +5,27 @@ "text": "{\n \"original_count\": 4,\n \"processed_count\": 4,\n \"action\": \"sort\",\n \"original\": [\n \"apple\",\n \"banana\",\n \"cherry\",\n \"date\"\n ],\n \"processed\": [\n \"apple\",\n \"banana\",\n \"cherry\",\n \"date\"\n ],\n \"stats\": {\n \"average_length\": 5.25,\n \"shortest\": 4,\n \"longest\": 6\n }\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "original_count": 4, + "processed_count": 4, + "action": "sort", + "original": [ + "apple", + "banana", + "cherry", + "date" + ], + "processed": [ + "apple", + "banana", + "cherry", + "date" + ], + "stats": { + "average_length": 5.25, + "shortest": 4, + "longest": 6 + } + } } diff --git a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-schedule_event.json b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-schedule_event.json index 924527dc..037cf172 100644 --- a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-schedule_event.json +++ b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-schedule_event.json @@ -5,5 +5,27 @@ "text": "{\n \"success\": true,\n \"event\": {\n \"id\": \"event_test123456789\",\n \"title\": \"Team Meeting\",\n \"start_time\": \"2025-01-01T00:00:00+00:00\",\n \"end_time\": \"2025-01-01T00:00:00+00:00\",\n \"duration_hours\": 1.5,\n \"priority\": \"high\",\n \"attendees\": [\n \"alice@example.com\",\n \"bob@example.com\"\n ],\n \"created_at\": \"2025-01-01T00:00:00+00:00\"\n },\n \"info\": {\n \"attendee_count\": 2,\n \"is_all_day\": false,\n \"is_future\": false,\n \"timezone_note\": \"Times are in UTC\"\n }\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "success": true, + "event": { + "id": "event_test123456789", + "title": "Team Meeting", + "start_time": "2025-01-01T00:00:00+00:00", + "end_time": "2025-01-01T00:00:00+00:00", + "duration_hours": 1.5, + "priority": "high", + "attendees": [ + "alice@example.com", + "bob@example.com" + ], + "created_at": "2025-01-01T00:00:00+00:00" + }, + "info": { + "attendee_count": 2, + "is_all_day": false, + "is_future": false, + "timezone_note": "Times are in UTC" + } + } } diff --git a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-validate_profile.json b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-validate_profile.json index 9fe2fa53..0f370e68 100644 --- a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-validate_profile.json +++ b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_call-validate_profile.json @@ -5,5 +5,17 @@ "text": "{\n \"valid\": true,\n \"profile\": {\n \"name\": \"John Doe\",\n \"email\": \"john@example.com\",\n \"age\": 30,\n \"role\": \"user\"\n },\n \"errors\": [],\n \"warnings\": [],\n \"processed_at\": \"2025-01-01 00:00:00\"\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "valid": true, + "profile": { + "name": "John Doe", + "email": "john@example.com", + "age": 30, + "role": "user" + }, + "errors": [], + "warnings": [], + "processed_at": "2025-01-01 00:00:00" + } } diff --git a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_list.json b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_list.json index 9b9b90e7..703e97af 100644 --- a/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_list.json +++ b/tests/Inspector/Http/snapshots/HttpSchemaShowcaseTest-tools_list.json @@ -28,6 +28,10 @@ "required": [ "text" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true } }, { @@ -73,6 +77,10 @@ "second", "operation" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true } }, { @@ -141,6 +149,10 @@ "required": [ "profile" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true } }, { @@ -178,6 +190,10 @@ "required": [ "items" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true } }, { @@ -225,6 +241,10 @@ "appName", "baseUrl" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true } }, { @@ -278,6 +298,10 @@ "startTime", "durationHours" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true } } ] diff --git a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-add_numbers.json b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-add_numbers.json index 3bb28b3d..2e04b001 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-add_numbers.json +++ b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-add_numbers.json @@ -5,5 +5,8 @@ "text": "8" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": 8 + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-add_numbers_negative.json b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-add_numbers_negative.json index 2a25b87b..0bf67735 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-add_numbers_negative.json +++ b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-add_numbers_negative.json @@ -5,5 +5,8 @@ "text": "-3" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": -3 + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-divide_numbers.json b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-divide_numbers.json index 957d6df4..9c494626 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-divide_numbers.json +++ b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-divide_numbers.json @@ -5,5 +5,8 @@ "text": "5" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": 5 + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-divide_numbers_decimal.json b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-divide_numbers_decimal.json index 1ae0005d..8008d493 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-divide_numbers_decimal.json +++ b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-divide_numbers_decimal.json @@ -5,5 +5,8 @@ "text": "3.5" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": 3.5 + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-multiply_numbers.json b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-multiply_numbers.json index b391c653..8b9291b9 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-multiply_numbers.json +++ b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-multiply_numbers.json @@ -5,5 +5,8 @@ "text": "24" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": 24 + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-multiply_numbers_zero.json b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-multiply_numbers_zero.json index 04988535..d288fbb7 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-multiply_numbers_zero.json +++ b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-multiply_numbers_zero.json @@ -5,5 +5,8 @@ "text": "0" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": 0 + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-power.json b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-power.json index d4289e41..5fd7af5a 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-power.json +++ b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-power.json @@ -5,5 +5,8 @@ "text": "256" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": 256 + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-power_zero_exponent.json b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-power_zero_exponent.json index 5088e95f..6d58bb70 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-power_zero_exponent.json +++ b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_call-power_zero_exponent.json @@ -5,5 +5,8 @@ "text": "1" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": 1 + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_list.json b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_list.json index 60848ab1..fd15afae 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_list.json +++ b/tests/Inspector/Stdio/snapshots/StdioCachedDiscoveryTest-tools_list.json @@ -16,6 +16,17 @@ "a", "b" ] + }, + "outputSchema": { + "type": "object", + "properties": { + "result": { + "type": "integer" + } + }, + "required": [ + "result" + ] } }, { @@ -34,6 +45,17 @@ "a", "b" ] + }, + "outputSchema": { + "type": "object", + "properties": { + "result": { + "type": "integer" + } + }, + "required": [ + "result" + ] } }, { @@ -52,6 +74,17 @@ "a", "b" ] + }, + "outputSchema": { + "type": "object", + "properties": { + "result": { + "type": "number" + } + }, + "required": [ + "result" + ] } }, { @@ -70,6 +103,17 @@ "base", "exponent" ] + }, + "outputSchema": { + "type": "object", + "properties": { + "result": { + "type": "integer" + } + }, + "required": [ + "result" + ] } } ] diff --git a/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-add_task.json b/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-add_task.json index 9ded3d2a..baff3cac 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-add_task.json +++ b/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-add_task.json @@ -5,5 +5,12 @@ "text": "{\n \"id\": 4,\n \"userId\": \"alice\",\n \"description\": \"Complete the project documentation\",\n \"completed\": false,\n \"createdAt\": \"2025-01-01T00:00:00+00:00\"\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "id": 4, + "userId": "alice", + "description": "Complete the project documentation", + "completed": false, + "createdAt": "2025-01-01T00:00:00+00:00" + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-complete_task.json b/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-complete_task.json index 3d852eda..2263660e 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-complete_task.json +++ b/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-complete_task.json @@ -5,5 +5,9 @@ "text": "{\n \"success\": true,\n \"message\": \"Task 1 completed.\"\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "success": true, + "message": "Task 1 completed." + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-list_user_tasks.json b/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-list_user_tasks.json index 6fac3026..5a740ce8 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-list_user_tasks.json +++ b/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_call-list_user_tasks.json @@ -5,5 +5,8 @@ "text": "[]" } ], - "isError": false + "isError": false, + "structuredContent": { + "items": [] + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_list.json b/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_list.json index 247b27fc..b2e3ab31 100644 --- a/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_list.json +++ b/tests/Inspector/Stdio/snapshots/StdioCustomDependenciesTest-tools_list.json @@ -19,6 +19,11 @@ "userId", "description" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true, + "description": "the created task details" } }, { @@ -35,6 +40,11 @@ "required": [ "userId" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true, + "description": "a list of tasks" } }, { @@ -51,6 +61,11 @@ "required": [ "taskId" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true, + "description": "status of the operation" } } ] diff --git a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call-calculate_sum.json b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call-calculate_sum.json index a73c8b94..bdfec0c0 100644 --- a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call-calculate_sum.json +++ b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call-calculate_sum.json @@ -5,5 +5,8 @@ "text": "19.8" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": 19.8 + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call-update_setting.json b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call-update_setting.json index 37b42155..7cda55e0 100644 --- a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call-update_setting.json +++ b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call-update_setting.json @@ -5,5 +5,9 @@ "text": "{\n \"success\": true,\n \"message\": \"Precision updated to 3.\"\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "success": true, + "message": "Precision updated to 3." + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call.json b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call.json index a73c8b94..bdfec0c0 100644 --- a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call.json +++ b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_call.json @@ -5,5 +5,8 @@ "text": "19.8" } ], - "isError": false + "isError": false, + "structuredContent": { + "result": 19.8 + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_list.json b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_list.json index 5f184117..34307c16 100644 --- a/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_list.json +++ b/tests/Inspector/Stdio/snapshots/StdioDiscoveryCalculatorTest-tools_list.json @@ -33,6 +33,18 @@ "b", "operation" ] + }, + "outputSchema": { + "type": "object", + "properties": { + "result": { + "type": "number" + } + }, + "required": [ + "result" + ], + "description": "the result of the calculation" } }, { @@ -53,6 +65,11 @@ "setting", "value" ] + }, + "outputSchema": { + "type": "object", + "additionalProperties": true, + "description": "success message or error" } } ] diff --git a/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_debug.json b/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_debug.json index 3b11d407..b046832e 100644 --- a/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_debug.json +++ b/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_debug.json @@ -5,5 +5,10 @@ "text": "{\n \"mode\": \"debug\",\n \"processed_input\": \"DEBUG TEST\",\n \"message\": \"Processed in DEBUG mode.\"\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "mode": "debug", + "processed_input": "DEBUG TEST", + "message": "Processed in DEBUG mode." + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_default.json b/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_default.json index fde189ee..af00a82b 100644 --- a/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_default.json +++ b/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_default.json @@ -5,5 +5,10 @@ "text": "{\n \"mode\": \"default\",\n \"original_input\": \"test data\",\n \"message\": \"Processed in default mode (APP_MODE not recognized or not set).\"\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "mode": "default", + "original_input": "test data", + "message": "Processed in default mode (APP_MODE not recognized or not set)." + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_production.json b/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_production.json index dd4cd9dc..4f30f8a0 100644 --- a/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_production.json +++ b/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_call-process_data_production.json @@ -5,5 +5,10 @@ "text": "{\n \"mode\": \"production\",\n \"processed_input_length\": 15,\n \"message\": \"Processed in PRODUCTION mode (summary only).\"\n}" } ], - "isError": false + "isError": false, + "structuredContent": { + "mode": "production", + "processed_input_length": 15, + "message": "Processed in PRODUCTION mode (summary only)." + } } diff --git a/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_list.json b/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_list.json index 32141675..f5d19c41 100644 --- a/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_list.json +++ b/tests/Inspector/Stdio/snapshots/StdioEnvVariablesTest-tools_list.json @@ -14,6 +14,31 @@ "required": [ "input" ] + }, + "outputSchema": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "description": "The processing mode used" + }, + "processed_input": { + "type": "string", + "description": "The processed input data" + }, + "original_input": { + "type": "string", + "description": "The original input data (only in default mode)" + }, + "message": { + "type": "string", + "description": "A descriptive message about the processing" + } + }, + "required": [ + "mode", + "message" + ] } } ] diff --git a/tests/Unit/Capability/Attribute/McpToolTest.php b/tests/Unit/Capability/Attribute/McpToolTest.php index e2814af7..b6ab86d5 100644 --- a/tests/Unit/Capability/Attribute/McpToolTest.php +++ b/tests/Unit/Capability/Attribute/McpToolTest.php @@ -30,14 +30,15 @@ public function testInstantiatesWithCorrectProperties(): void $this->assertSame($description, $attribute->description); } - public function testInstantiatesWithNullValuesForNameAndDescription(): void + public function testInstantiatesWithNullValuesForNameDescriptionAndOutputSchema(): void { // Arrange & Act - $attribute = new McpTool(name: null, description: null); + $attribute = new McpTool(name: null, description: null, outputSchema: null); // Assert $this->assertNull($attribute->name); $this->assertNull($attribute->description); + $this->assertNull($attribute->outputSchema); } public function testInstantiatesWithMissingOptionalArguments(): void @@ -48,5 +49,31 @@ public function testInstantiatesWithMissingOptionalArguments(): void // Assert $this->assertNull($attribute->name); $this->assertNull($attribute->description); + $this->assertNull($attribute->outputSchema); + } + + public function testInstantiatesWithOutputSchema(): void + { + // Arrange + $name = 'test-tool-name'; + $description = 'This is a test description.'; + $outputSchema = [ + 'type' => 'object', + 'properties' => [ + 'result' => [ + 'type' => 'string', + 'description' => 'The result of the operation', + ], + ], + 'required' => ['result'], + ]; + + // Act + $attribute = new McpTool(name: $name, description: $description, outputSchema: $outputSchema); + + // Assert + $this->assertSame($name, $attribute->name); + $this->assertSame($description, $attribute->description); + $this->assertSame($outputSchema, $attribute->outputSchema); } } diff --git a/tests/Unit/Capability/Discovery/SchemaGeneratorFixture.php b/tests/Unit/Capability/Discovery/SchemaGeneratorFixture.php index 5a7fcaeb..125d0189 100644 --- a/tests/Unit/Capability/Discovery/SchemaGeneratorFixture.php +++ b/tests/Unit/Capability/Discovery/SchemaGeneratorFixture.php @@ -412,4 +412,53 @@ public function parameterSchemaInferredType( $inferredParam, ): void { } + + // ===== OUTPUT SCHEMA FIXTURES ===== + public function stringReturn(): string + { + return 'test'; + } + + public function integerReturn(): int + { + return 42; + } + + public function floatReturn(): float + { + return 3.14; + } + + public function booleanReturn(): bool + { + return true; + } + + public function nullableReturn(): ?string + { + return null; + } + + public function objectReturn(): \stdClass + { + return new \stdClass(); + } + + public function docBlockReturnType(): string + { + return '42'; + } + + public function unionReturn(): float|string + { + return 'test'; + } + + /** + * @return string The result of the operation + */ + public function returnWithDescription(): string + { + return 'result'; + } } diff --git a/tests/Unit/Capability/Discovery/SchemaGeneratorTest.php b/tests/Unit/Capability/Discovery/SchemaGeneratorTest.php index 4cbfce52..c8cd4bbc 100644 --- a/tests/Unit/Capability/Discovery/SchemaGeneratorTest.php +++ b/tests/Unit/Capability/Discovery/SchemaGeneratorTest.php @@ -327,4 +327,102 @@ public function testInfersParameterTypeAsAnyIfOnlyConstraintsAreGiven() $this->assertEquals(['description' => 'Some parameter', 'minLength' => 3], $schema['properties']['inferredParam']); $this->assertEquals(['inferredParam'], $schema['required']); } + + public function testGenerateOutputSchemaReturnsNullForVoidReturnType(): void + { + $method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'noParams'); + $schema = $this->schemaGenerator->generateOutputSchema($method); + $this->assertNull($schema); + } + + /** + * @dataProvider providesAllOutputSchemaResultReturnTypes + */ + public function testGenerateOutputSchemaForBasicReturnTypes(string $methodName, string $expectedType): void + { + $method = new \ReflectionMethod(SchemaGeneratorFixture::class, $methodName); + $schema = $this->schemaGenerator->generateOutputSchema($method); + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'result' => ['type' => $expectedType], + ], + 'required' => ['result'], + ], $schema); + } + + public function testGenerateOutputSchemaWithReturnDescription(): void + { + $method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'returnWithDescription'); + $schema = $this->schemaGenerator->generateOutputSchema($method); + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'result' => ['type' => 'string'], + ], + 'required' => ['result'], + 'description' => 'The result of the operation', + ], $schema); + } + + public function testGenerateOutputSchemaForArrayReturnType(): void + { + $method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'noParamsWithSchema'); + $schema = $this->schemaGenerator->generateOutputSchema($method); + $this->assertEquals([ + 'type' => 'object', + 'additionalProperties' => true, + ], $schema); + } + + public function testGenerateOutputSchemaForUnionReturnType(): void + { + $method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'unionReturn'); + $schema = $this->schemaGenerator->generateOutputSchema($method); + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'result' => ['type' => ['string', 'number']], + ], + 'required' => ['result'], + ], $schema); + } + + public function testGenerateOutputSchemaUsesPhpTypeHintOverDocBlock(): void + { + $method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'docBlockReturnType'); + $schema = $this->schemaGenerator->generateOutputSchema($method); + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'result' => ['type' => 'string'], + ], + 'required' => ['result'], + ], $schema); + } + + public function testGenerateOutputSchemaForComplexNestedSchema(): void + { + $method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'complexNestedSchema'); + $schema = $this->schemaGenerator->generateOutputSchema($method); + $this->assertEquals([ + 'type' => 'object', + 'additionalProperties' => true, + ], $schema); + } + + /** + * @return array + */ + public static function providesAllOutputSchemaResultReturnTypes(): array + { + return [ + 'string' => ['stringReturn', 'string'], + 'integer' => ['integerReturn', 'integer'], + 'float' => ['floatReturn', 'number'], + 'boolean' => ['booleanReturn', 'boolean'], + 'nullable string' => ['nullableReturn', 'string'], + 'object' => ['objectReturn', 'object'], + ]; + } } diff --git a/tests/Unit/Capability/RegistryTest.php b/tests/Unit/Capability/RegistryTest.php index d97ccf41..6b9416f5 100644 --- a/tests/Unit/Capability/RegistryTest.php +++ b/tests/Unit/Capability/RegistryTest.php @@ -527,7 +527,72 @@ public function testMultipleRegistrationsOfSameElementWithSameType(): void $this->assertEquals('second', ($toolRef->handler)()); } - private function createValidTool(string $name): Tool + public function testExtractStructuredContentReturnsNullWhenOutputSchemaIsNull(): void + { + $tool = $this->createValidTool('test_tool', null); + $this->registry->registerTool($tool, fn () => 'result'); + + $toolRef = $this->registry->getTool('test_tool'); + $this->assertNull($toolRef->extractStructuredContent('result')); + } + + public function testExtractStructuredContentWrapsScalarInResultKey(): void + { + $tool = $this->createValidTool('test_tool', [ + 'type' => 'object', + 'properties' => [ + 'result' => ['type' => 'string'], + ], + ]); + $this->registry->registerTool($tool, fn () => 'hello'); + + $toolRef = $this->registry->getTool('test_tool'); + $this->assertEquals(['result' => 'hello'], $toolRef->extractStructuredContent('hello')); + } + + public function testExtractStructuredContentWrapsArrayInResultKey(): void + { + $tool = $this->createValidTool('test_tool', [ + 'type' => 'object', + 'properties' => [ + 'result' => ['type' => 'object'], + ], + ]); + $this->registry->registerTool($tool, fn () => ['key' => 'value']); + + $toolRef = $this->registry->getTool('test_tool'); + $this->assertEquals(['result' => ['key' => 'value']], $toolRef->extractStructuredContent(['key' => 'value'])); + } + + public function testExtractStructuredContentReturnsArrayDirectlyForAdditionalProperties(): void + { + $tool = $this->createValidTool('test_tool', [ + 'type' => 'object', + 'additionalProperties' => true, + ]); + $this->registry->registerTool($tool, fn () => ['success' => true, 'message' => 'done']); + + $toolRef = $this->registry->getTool('test_tool'); + $this->assertEquals(['success' => true, 'message' => 'done'], $toolRef->extractStructuredContent(['success' => true, 'message' => 'done'])); + } + + public function testExtractStructuredContentNormalizesObjectsToArrays(): void + { + $tool = $this->createValidTool('test_tool', [ + 'type' => 'object', + 'additionalProperties' => true, + ]); + $object = new \stdClass(); + $object->key = 'value'; + $this->registry->registerTool($tool, fn () => $object); + + $toolRef = $this->registry->getTool('test_tool'); + $result = $toolRef->extractStructuredContent($object); + $this->assertIsArray($result); + $this->assertEquals(['key' => 'value'], $result); + } + + private function createValidTool(string $name, ?array $outputSchema = null): Tool { return new Tool( name: $name, @@ -540,6 +605,9 @@ private function createValidTool(string $name): Tool ], description: "Test tool: {$name}", annotations: null, + icons: null, + meta: null, + outputSchema: $outputSchema ); } diff --git a/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php b/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php index 5b03f2bb..d48f1687 100644 --- a/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php +++ b/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php @@ -21,6 +21,7 @@ use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\Request\CallToolRequest; use Mcp\Schema\Result\CallToolResult; +use Mcp\Schema\Tool; use Mcp\Server\Handler\Request\CallToolHandler; use Mcp\Server\Session\SessionInterface; use PHPUnit\Framework\MockObject\MockObject; @@ -59,7 +60,9 @@ public function testSupportsCallToolRequest(): void public function testHandleSuccessfulToolCall(): void { $request = $this->createCallToolRequest('greet_user', ['name' => 'John']); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); $expectedResult = new CallToolResult([new TextContent('Hello, John!')]); $this->registry @@ -92,7 +95,9 @@ public function testHandleSuccessfulToolCall(): void public function testHandleToolCallWithEmptyArguments(): void { $request = $this->createCallToolRequest('simple_tool', []); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); $expectedResult = new CallToolResult([new TextContent('Simple result')]); $this->registry @@ -129,7 +134,9 @@ public function testHandleToolCallWithComplexArguments(): void 'null_param' => null, ]; $request = $this->createCallToolRequest('complex_tool', $arguments); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); $expectedResult = new CallToolResult([new TextContent('Complex result')]); $this->registry @@ -182,7 +189,9 @@ public function testHandleToolCallExceptionReturnsResponseWithErrorResult(): voi $request = $this->createCallToolRequest('failing_tool', ['param' => 'value']); $exception = new ToolCallException('Tool execution failed'); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); $this->registry ->expects($this->once()) ->method('getTool') @@ -217,7 +226,9 @@ public function testHandleWithNullResult(): void $request = $this->createCallToolRequest('null_tool', []); $expectedResult = new CallToolResult([]); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); $this->registry ->expects($this->once()) ->method('getTool') @@ -254,7 +265,9 @@ public function testHandleLogsErrorWithCorrectParameters(): void $request = $this->createCallToolRequest('test_tool', ['key1' => 'value1', 'key2' => 42]); $exception = new ToolCallException('Custom error message'); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); $this->registry ->expects($this->once()) ->method('getTool') @@ -297,7 +310,9 @@ public function testHandleGenericExceptionReturnsError(): void $request = $this->createCallToolRequest('failing_tool', ['param' => 'value']); $exception = new \RuntimeException('Internal database connection failed'); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); $this->registry ->expects($this->once()) ->method('getTool') @@ -324,7 +339,10 @@ public function testHandleWithSpecialCharactersInToolName(): void $request = $this->createCallToolRequest('tool-with_special.chars', []); $expectedResult = new CallToolResult([new TextContent('Special tool result')]); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); + $this->registry ->expects($this->once()) ->method('getTool') @@ -359,7 +377,9 @@ public function testHandleWithSpecialCharactersInArguments(): void $request = $this->createCallToolRequest('unicode_tool', $arguments); $expectedResult = new CallToolResult([new TextContent('Unicode handled')]); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); $this->registry ->expects($this->once()) ->method('getTool') @@ -387,7 +407,9 @@ public function testHandleWithSpecialCharactersInArguments(): void public function testHandleReturnsStructuredContentResult(): void { $request = $this->createCallToolRequest('structured_tool', ['query' => 'php']); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); $structuredResult = new CallToolResult([new TextContent('Rendered results')], false, ['result' => 'Rendered results']); $this->registry @@ -416,7 +438,9 @@ public function testHandleReturnsStructuredContentResult(): void public function testHandleReturnsCallToolResult(): void { $request = $this->createCallToolRequest('result_tool', ['query' => 'php']); - $toolReference = $this->createMock(ToolReference::class); + $toolReference = $this->createToolReference('greet_user', function () { + return 'Hello, John!'; + }); $callToolResult = new CallToolResult([new TextContent('Error result')], true); $this->registry @@ -457,4 +481,22 @@ private function createCallToolRequest(string $name, array $arguments): CallTool ], ]); } + + private function createToolReference( + string $name, + callable $handler, + ?array $outputSchema = null, + array $methodsToMock = ['formatResult'], + ): ToolReference&MockObject { + $tool = new Tool($name, ['type' => 'object', 'properties' => [], 'required' => null], null, null, null, null, $outputSchema); + + $builder = $this->getMockBuilder(ToolReference::class) + ->setConstructorArgs([$tool, $handler]); + + if (!empty($methodsToMock)) { + $builder->onlyMethods($methodsToMock); + } + + return $builder->getMock(); + } }