diff --git a/examples/http_server.php b/examples/http_server.php index 36fc735..1b05274 100755 --- a/examples/http_server.php +++ b/examples/http_server.php @@ -19,12 +19,15 @@ #[ToolAttribute(name: "echo", description: "Echoes back the provided message.")] class EchoTool extends BaseTool { + /** + * @return \MCP\Server\Tool\Content\ContentItemInterface + */ protected function doExecute( #[Parameter(name: 'message', type: 'string', description: 'The message to echo.')] array $arguments - ): array { + ): \MCP\Server\Tool\Content\ContentItemInterface { $message = $arguments['message'] ?? 'Default message if not provided'; - return [$this->createTextContent("Echo: " . $message)]; + return $this->text("Echo: " . $message); } } diff --git a/examples/stdio_server.php b/examples/stdio_server.php index b1fbc34..38fd769 100755 --- a/examples/stdio_server.php +++ b/examples/stdio_server.php @@ -16,12 +16,15 @@ #[ToolAttribute(name: "echo", description: "Echoes back the provided message.")] class EchoTool extends BaseTool { + /** + * @return \MCP\Server\Tool\Content\ContentItemInterface + */ protected function doExecute( #[Parameter(name: 'message', type: 'string', description: 'The message to echo.')] array $arguments - ): array { + ): \MCP\Server\Tool\Content\ContentItemInterface { $message = $arguments['message'] ?? 'Default message if not provided'; - return [$this->createTextContent("Echo: " . $message)]; + return $this->text("Echo: " . $message); } } diff --git a/src/Tool/Tool.php b/src/Tool/Tool.php index 229bcad..3c5ef27 100644 --- a/src/Tool/Tool.php +++ b/src/Tool/Tool.php @@ -202,19 +202,16 @@ public function shutdown(): void final public function execute(array $arguments): array { $this->validateArguments($arguments); - $contentItems = $this->doExecute($arguments); + $returnedContent = $this->doExecute($arguments); // Can be single item or array + $contentItems = is_array($returnedContent) ? $returnedContent : [$returnedContent]; $resultArray = []; + foreach ($contentItems as $item) { if (!$item instanceof Content\ContentItemInterface) { - // Or throw an exception, depending on how strict we want to be - // For now, let's assume doExecute correctly returns - // ContentItemInterface objects and skip invalid items if any - // for robustness. - // A stricter approach might be: - // throw new \LogicException( - // 'doExecute must return an array of ContentItemInterface objects.' - // ); - continue; + // Throw an exception for stricter validation, ensuring all items are correct + throw new \LogicException( + 'All items returned by doExecute must be instances of ContentItemInterface.' + ); } $resultArray[] = $item->toArray(); } @@ -278,14 +275,20 @@ private function validateType($value, string $type): bool * Executes the core logic of the tool. * * This method must be implemented by concrete tool classes. It receives - * validated arguments and should return an array of ContentItemInterface + * validated arguments and should return one or more ContentItemInterface * objects representing the tool's output. * + * If a single ContentItemInterface object is returned, the `execute()` method + * will automatically wrap it in an array. This ensures that the final tool + * response sent by the server adheres to the protocol, which expects an array + * of content items. + * * @param array $arguments Validated arguments for the tool, matching the defined parameters. - * @return Content\ContentItemInterface[] An array of content items (e.g., TextContent, ImageContent) + * @return Content\ContentItemInterface[]|Content\ContentItemInterface An array of content items + * (e.g., TextContent, ImageContent) or a single content item * representing the tool's response. */ - abstract protected function doExecute(array $arguments): array; + abstract protected function doExecute(array $arguments): array|Content\ContentItemInterface; // Content Creation Helper Methods @@ -297,7 +300,7 @@ abstract protected function doExecute(array $arguments): array; * @param Content\Annotations|null $annotations Optional annotations for the text content. * @return Content\TextContent The created TextContent item. */ - final protected function createTextContent( + final protected function text( string $text, ?Content\Annotations $annotations = null ): Content\TextContent { @@ -314,7 +317,7 @@ final protected function createTextContent( * @param Content\Annotations|null $annotations Optional annotations for the image content. * @return Content\ImageContent The created ImageContent item. */ - final protected function createImageContent( + final protected function image( string $rawData, string $mimeType, ?Content\Annotations $annotations = null @@ -336,7 +339,7 @@ final protected function createImageContent( * @param Content\Annotations|null $annotations Optional annotations for the audio content. * @return Content\AudioContent The created AudioContent item. */ - final protected function createAudioContent( + final protected function audio( string $rawData, string $mimeType, ?Content\Annotations $annotations = null @@ -357,7 +360,7 @@ final protected function createAudioContent( * @param Content\Annotations|null $annotations Optional annotations for the embedded resource. * @return Content\EmbeddedResource The created EmbeddedResource item. */ - final protected function createEmbeddedResource( + final protected function embeddedResource( array $resourceData, ?Content\Annotations $annotations = null ): Content\EmbeddedResource { diff --git a/tests/Capability/FailingMockTool.php b/tests/Capability/FailingMockTool.php index 483021b..1b79f31 100644 --- a/tests/Capability/FailingMockTool.php +++ b/tests/Capability/FailingMockTool.php @@ -4,10 +4,15 @@ use MCP\Server\Tool\Tool; use MCP\Server\Tool\Attribute\Tool as ToolAttribute; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAttribute('failing', 'Failing Tool')] class FailingMockTool extends Tool { + /** + * @return array + * @throws \RuntimeException Always throws. + */ protected function doExecute(array $arguments): array { throw new \RuntimeException('Tool execution failed'); diff --git a/tests/Capability/InvalidSuggestionsTool.php b/tests/Capability/InvalidSuggestionsTool.php index 1cf8eaa..721ccd6 100644 --- a/tests/Capability/InvalidSuggestionsTool.php +++ b/tests/Capability/InvalidSuggestionsTool.php @@ -4,6 +4,7 @@ use MCP\Server\Tool\Attribute\Tool as ToolAttribute; use MCP\Server\Tool\Tool; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAttribute('invalidSuggestionsTool', 'Tool that returns invalid suggestions')] class InvalidSuggestionsTool extends Tool @@ -18,6 +19,9 @@ public function __construct(array $suggestionsToReturn) $this->suggestionsToReturn = $suggestionsToReturn; } + /** + * @return array + */ protected function doExecute(array $arguments): array { // Not used for completion tests diff --git a/tests/Capability/MockTool.php b/tests/Capability/MockTool.php index e0dd226..4399f98 100644 --- a/tests/Capability/MockTool.php +++ b/tests/Capability/MockTool.php @@ -6,16 +6,20 @@ use MCP\Server\Tool\Attribute\Tool as ToolAttribute; use MCP\Server\Tool\Attribute\Parameter as ParameterAttribute; use MCP\Server\Tool\Attribute\ToolAnnotations; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAnnotations(title: 'Mock Test Tool', readOnlyHint: true)] #[ToolAttribute('test', 'Test Tool')] class MockTool extends Tool { + /** + * @return ContentItemInterface + */ protected function doExecute( #[ParameterAttribute('data', type: 'string', description: 'Input data')] array $arguments - ): array { - return [$this->createTextContent('Result: ' . $arguments['data'])]; + ): \MCP\Server\Tool\Content\ContentItemInterface { + return $this->text('Result: ' . $arguments['data']); } /** diff --git a/tests/Capability/ToolsCapabilityTest.php b/tests/Capability/ToolsCapabilityTest.php index 0435d48..a9010da 100644 --- a/tests/Capability/ToolsCapabilityTest.php +++ b/tests/Capability/ToolsCapabilityTest.php @@ -253,9 +253,12 @@ public function shutdown(): void { $this->shutdownCountRef++; } - protected function doExecute(array $arguments): array + /** + * @return Content\ContentItemInterface + */ + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface { - return [$this->createTextContent('done')]; + return $this->text('done'); } }; @@ -312,9 +315,12 @@ public function testHandleCompleteWithNullIdIsNotification(): void public function testHandleCompleteDefaultSuggestions(): void { $basicTool = new #[ToolAttribute('basic', 'Basic Tool')] class extends Tool { - protected function doExecute(array $arguments): array + /** + * @return Content\ContentItemInterface + */ + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface { - return [$this->createTextContent('done')]; + return $this->text('done'); } }; $this->capability->addTool($basicTool); // Add to the existing capability instance @@ -447,6 +453,9 @@ public function testHandleCompleteWithToolReturningInvalidSuggestionsValuesType( // if constructor validation is robust. $customBadTool = new #[ToolAttribute('customBadValuesTool', 'Tool with bad values type')] class extends Tool { + /** + * @return array + */ protected function doExecute(array $arguments): array { return []; @@ -481,6 +490,9 @@ public function getCompletionSuggestions(string $argumentName, mixed $currentVal public function testHandleCompleteWithToolReturningNonArraySuggestions(): void { $nonArraySuggestionsTool = new #[ToolAttribute('nonArraySuggestionsTool', 'Tool returning non-array suggestions')] class extends Tool { + /** + * @return array + */ protected function doExecute(array $arguments): array { return []; // Not called in this test path diff --git a/tests/Tool/ArrayParamTool.php b/tests/Tool/ArrayParamTool.php index 28b7c4e..3fb4d16 100644 --- a/tests/Tool/ArrayParamTool.php +++ b/tests/Tool/ArrayParamTool.php @@ -5,19 +5,23 @@ use MCP\Server\Tool\Tool; use MCP\Server\Tool\Attribute\Tool as ToolAttribute; use MCP\Server\Tool\Attribute\Parameter as ParameterAttribute; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAttribute('array-tool', 'Tests array parameters')] class ArrayParamTool extends Tool { + /** + * @return ContentItemInterface + */ protected function doExecute( #[ParameterAttribute('numbers', type: 'array', description: 'List of numbers')] #[ParameterAttribute('enabled', type: 'boolean', description: 'Whether processing is enabled')] array $arguments - ): array { + ): \MCP\Server\Tool\Content\ContentItemInterface { if (!$arguments['enabled']) { - return [$this->createTextContent('Processing disabled')]; + return $this->text('Processing disabled'); } $sum = array_sum($arguments['numbers']); - return [$this->createTextContent("Sum: $sum")]; + return $this->text("Sum: $sum"); } } diff --git a/tests/Tool/CalculatorTool.php b/tests/Tool/CalculatorTool.php index ca9425c..b72213f 100644 --- a/tests/Tool/CalculatorTool.php +++ b/tests/Tool/CalculatorTool.php @@ -5,22 +5,26 @@ use MCP\Server\Tool\Tool; use MCP\Server\Tool\Attribute\Tool as ToolAttribute; use MCP\Server\Tool\Attribute\Parameter as ParameterAttribute; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAttribute('calculator', 'A calculator tool')] class CalculatorTool extends Tool { + /** + * @return ContentItemInterface + */ protected function doExecute( #[ParameterAttribute('operation', type: 'string', description: 'Operation to perform (add/subtract)')] #[ParameterAttribute('a', type: 'number', description: 'First number')] #[ParameterAttribute('b', type: 'number', description: 'Second number')] array $arguments - ): array { + ): \MCP\Server\Tool\Content\ContentItemInterface { $result = match ($arguments['operation']) { 'add' => $arguments['a'] + $arguments['b'], 'subtract' => $arguments['a'] - $arguments['b'], default => throw new \InvalidArgumentException('Invalid operation') }; - return [$this->createTextContent((string)$result)]; + return $this->text((string)$result); } } diff --git a/tests/Tool/Helpers/AbstractTestTool.php b/tests/Tool/Helpers/AbstractTestTool.php index e19bcd4..5d30805 100644 --- a/tests/Tool/Helpers/AbstractTestTool.php +++ b/tests/Tool/Helpers/AbstractTestTool.php @@ -6,9 +6,13 @@ use MCP\Server\Tool\Tool; use MCP\Server\Tool\Attribute\Tool as ToolAttribute; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAttribute('abstract-test-tool', 'Abstract Test Tool')] abstract class AbstractTestTool extends Tool { + /** + * @return array + */ abstract protected function doExecute(array $arguments): array; } diff --git a/tests/Tool/Helpers/NoAttributeTool.php b/tests/Tool/Helpers/NoAttributeTool.php index 09b34d0..4c5a950 100644 --- a/tests/Tool/Helpers/NoAttributeTool.php +++ b/tests/Tool/Helpers/NoAttributeTool.php @@ -5,9 +5,13 @@ namespace MCP\Server\Tests\Tool\Helpers; use MCP\Server\Tool\Tool; +use MCP\Server\Tool\Content\ContentItemInterface; class NoAttributeTool extends Tool { + /** + * @return array + */ protected function doExecute(array $arguments): array { return []; // Dummy implementation diff --git a/tests/Tool/MockTool.php b/tests/Tool/MockTool.php index 542ff41..d36739f 100644 --- a/tests/Tool/MockTool.php +++ b/tests/Tool/MockTool.php @@ -5,12 +5,16 @@ use MCP\Server\Tool\Tool; use MCP\Server\Tool\Attribute\Tool as ToolAttribute; use MCP\Server\Tool\Attribute\Parameter as ParameterAttribute; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAttribute('test', 'Test Tool')] class MockTool extends Tool { - protected function doExecute(array $arguments): array + /** + * @return ContentItemInterface + */ + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface { - return [$this->createTextContent('Hello World')]; + return $this->text('Hello World'); } } diff --git a/tests/Tool/MultiOutputTool.php b/tests/Tool/MultiOutputTool.php index ab5d67d..74a1433 100644 --- a/tests/Tool/MultiOutputTool.php +++ b/tests/Tool/MultiOutputTool.php @@ -5,17 +5,21 @@ use MCP\Server\Tool\Tool; use MCP\Server\Tool\Attribute\Tool as ToolAttribute; use MCP\Server\Tool\Attribute\Parameter as ParameterAttribute; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAttribute('multi-output', 'Tests multiple output types')] class MultiOutputTool extends Tool { + /** + * @return ContentItemInterface + */ protected function doExecute( #[ParameterAttribute('format', type: 'string', description: 'Output format (text/image)')] array $arguments - ): array { + ): \MCP\Server\Tool\Content\ContentItemInterface { if ($arguments['format'] === 'text') { - return [$this->createTextContent('Hello world')]; + return $this->text('Hello world'); } - return [$this->createImageContent('fake-image-data', 'image/png')]; + return $this->image('fake-image-data', 'image/png'); } } diff --git a/tests/Tool/OptionalParamTool.php b/tests/Tool/OptionalParamTool.php index 5d95ba9..3a4dcc4 100644 --- a/tests/Tool/OptionalParamTool.php +++ b/tests/Tool/OptionalParamTool.php @@ -5,16 +5,20 @@ use MCP\Server\Tool\Tool; use MCP\Server\Tool\Attribute\Tool as ToolAttribute; use MCP\Server\Tool\Attribute\Parameter as ParameterAttribute; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAttribute('greeter', 'A friendly greeter')] class OptionalParamTool extends Tool { + /** + * @return ContentItemInterface + */ protected function doExecute( #[ParameterAttribute('name', type: 'string', description: 'Name to greet')] #[ParameterAttribute('title', type: 'string', description: 'Optional title', required: false)] array $arguments - ): array { + ): \MCP\Server\Tool\Content\ContentItemInterface { $title = $arguments['title'] ?? 'friend'; - return [$this->createTextContent("Hello {$title} {$arguments['name']}")]; + return $this->text("Hello {$title} {$arguments['name']}"); } } diff --git a/tests/Tool/OtherMockTool.php b/tests/Tool/OtherMockTool.php index 4d07b10..6a21e7d 100644 --- a/tests/Tool/OtherMockTool.php +++ b/tests/Tool/OtherMockTool.php @@ -7,12 +7,16 @@ // ParameterAttribute is not used in this class, so it's not strictly necessary to import, // but kept for consistency if it might be added later or to avoid confusion. use MCP\Server\Tool\Attribute\Parameter as ParameterAttribute; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAttribute('other', 'Other Tool')] class OtherMockTool extends Tool { - protected function doExecute(array $arguments): array + /** + * @return ContentItemInterface + */ + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface { - return [$this->createTextContent('Other Result')]; + return $this->text('Other Result'); } } diff --git a/tests/Tool/TestTool.php b/tests/Tool/TestTool.php index dc8fd56..4fcb62b 100644 --- a/tests/Tool/TestTool.php +++ b/tests/Tool/TestTool.php @@ -5,14 +5,18 @@ use MCP\Server\Tool\Tool; use MCP\Server\Tool\Attribute\Tool as ToolAttribute; use MCP\Server\Tool\Attribute\Parameter as ParameterAttribute; +use MCP\Server\Tool\Content\ContentItemInterface; #[ToolAttribute('test.tool', 'A test tool')] class TestTool extends Tool { + /** + * @return ContentItemInterface + */ protected function doExecute( #[ParameterAttribute('name', type: 'string', description: 'Name to greet')] array $arguments - ): array { - return [$this->createTextContent('Hello ' . $arguments['name'])]; + ): \MCP\Server\Tool\Content\ContentItemInterface { + return $this->text('Hello ' . $arguments['name']); } } diff --git a/tests/Tool/ToolTest.php b/tests/Tool/ToolTest.php index 22cfca5..a06bbf7 100644 --- a/tests/Tool/ToolTest.php +++ b/tests/Tool/ToolTest.php @@ -6,6 +6,7 @@ use MCP\Server\Tool\Attribute\Tool as ToolAttribute; use MCP\Server\Tool\Attribute\Parameter as ParameterAttribute; use PHPUnit\Framework\TestCase; +use MCP\Server\Tool\Content\ContentItemInterface; // TestTool and CalculatorTool are now in separate files. @@ -276,4 +277,111 @@ private function validateAgainstSchema(mixed $data, array $schema): bool } return true; } + + public function testExecuteWithSingleTextContentReturn(): void + { + $tool = new class extends Tool { + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface + { + return $this->text('Hello world'); + } + }; + + $result = $tool->execute([]); + $this->assertEquals([['type' => 'text', 'text' => 'Hello world']], $result); + } + + public function testExecuteWithSingleImageContentReturn(): void + { + $tool = new class extends Tool { + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface + { + return $this->image('fakedata', 'image/png'); + } + }; + + $result = $tool->execute([]); + $this->assertEquals([['type' => 'image', 'data' => base64_encode('fakedata'), 'mimeType' => 'image/png']], $result); + } + + public function testExecuteWithSingleAudioContentReturn(): void + { + $tool = new class extends Tool { + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface + { + return $this->audio('fakedata', 'audio/mp3'); + } + }; + + $result = $tool->execute([]); + $this->assertEquals([['type' => 'audio', 'data' => base64_encode('fakedata'), 'mimeType' => 'audio/mp3']], $result); + } + + public function testExecuteWithSingleEmbeddedResourceReturn(): void + { + $tool = new class extends Tool { + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface + { + return $this->embeddedResource(['uri' => '/my/res', 'text' => 'res text']); + } + }; + + $result = $tool->execute([]); + $this->assertEquals([['type' => 'resource', 'resource' => ['uri' => '/my/res', 'text' => 'res text']]], $result); + } + + public function testExecuteWithMultipleContentItemsReturn(): void + { + $tool = new class extends Tool { + /** + * @return array<\MCP\Server\Tool\Content\ContentItemInterface> + */ + protected function doExecute(array $arguments): array + { + return [ + $this->text('Hello'), + $this->image('fakedata', 'image/jpeg') + ]; + } + }; + + $result = $tool->execute([]); + $this->assertEquals( + [['type' => 'text', 'text' => 'Hello'], ['type' => 'image', 'data' => base64_encode('fakedata'), 'mimeType' => 'image/jpeg']], + $result + ); + } + + public function testExecuteWithInvalidReturnFromDoExecuteNotContentItem(): void + { + $tool = new class extends Tool { + protected function doExecute(array $arguments): array|\MCP\Server\Tool\Content\ContentItemInterface + { + // @phpstan-ignore-next-line Deliberately returning invalid type for test. + return 'not a content item'; + } + }; + + $this->expectException(\TypeError::class); + // $this->expectExceptionMessage('doExecute must return an array of ContentItemInterface objects or a single ContentItemInterface object.'); + $tool->execute([]); + } + + public function testExecuteWithInvalidReturnFromDoExecuteArrayWithInvalidItem(): void + { + $tool = new class extends Tool { + /** + * @return array<\MCP\Server\Tool\Content\ContentItemInterface> + */ + protected function doExecute(array $arguments): array + { + // @phpstan-ignore-next-line Deliberately returning invalid type in array for test. + return [$this->text("valid"), "invalid"]; + } + }; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('All items returned by doExecute must be instances of ContentItemInterface.'); + $tool->execute([]); + } }