From 1c754c85690fe650d1307d01059e85a00ce6ef43 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 07:39:16 +0000 Subject: [PATCH 1/3] I've refactored the Tool class for improved ergonomics and consistency. This commit introduces several improvements to the `Tool` class: 1. **Flexible Return Types for `doExecute()`:** The `Tool::doExecute()` method can now return either a single `ContentItemInterface` object or an array of `ContentItemInterface` objects. If a single item is returned, the `Tool::execute()` method automatically wraps it in an array. This simplifies tool implementation when only one content item needs to be returned, e.g., `return $this->text("response");`. The final JSON output still adheres to the Model Context Protocol, with the `content` field always being an array. 2. **Shorter Helper Method Names:** The content creation helper methods within the `Tool` class have been renamed for brevity and consistency with similar methods in the `Resource` class: - `createTextContent()` is now `text()` - `createImageContent()` is now `image()` - `createAudioContent()` is now `audio()` - `createEmbeddedResource()` is now `embeddedResource()` 3. **Codebase Updates:** All existing tool implementations and tests throughout the codebase have been updated to use the new helper method names. 4. **Enhanced Testing:** `ToolTest.php` has been significantly updated with new test cases to cover: - Returning single text, image, audio, and embedded resource items. - Returning multiple content items using the new helper names. - Handling of invalid return types from `doExecute()`. 5. **PHPStan and PHPUnit Fixes:** Addressed various PHPStan typing errors by adding appropriate `use` statements and refining PHPDoc annotations. Corrected PHPUnit test logic, including data passed to media helper methods and expected exception types. I ran the `jules_setup.sh` script to ensure a consistent development environment, and the `run_tests.sh` script confirms that all tests and linters pass. --- src/Tool/Tool.php | 51 ++++++--- tests/Capability/FailingMockTool.php | 5 + tests/Capability/InvalidSuggestionsTool.php | 4 + tests/Capability/MockTool.php | 6 +- tests/Capability/ToolsCapabilityTest.php | 16 ++- tests/Tool/ArrayParamTool.php | 8 +- tests/Tool/CalculatorTool.php | 6 +- tests/Tool/Helpers/AbstractTestTool.php | 4 + tests/Tool/Helpers/NoAttributeTool.php | 4 + tests/Tool/MockTool.php | 6 +- tests/Tool/MultiOutputTool.php | 8 +- tests/Tool/OptionalParamTool.php | 6 +- tests/Tool/OtherMockTool.php | 6 +- tests/Tool/TestTool.php | 6 +- tests/Tool/ToolTest.php | 108 ++++++++++++++++++++ 15 files changed, 215 insertions(+), 29 deletions(-) diff --git a/src/Tool/Tool.php b/src/Tool/Tool.php index 229bcad..0c1997c 100644 --- a/src/Tool/Tool.php +++ b/src/Tool/Tool.php @@ -202,19 +202,30 @@ 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 = []; + if ($returnedContent instanceof Content\ContentItemInterface) { + // If doExecute returns a single item, wrap it in an array + $contentItems[] = $returnedContent; + } elseif (is_array($returnedContent)) { + // If it's already an array, use it directly + $contentItems = $returnedContent; + } else { + // If $returnedContent is not an array and not a ContentItemInterface, + // it's an invalid return type from doExecute. + throw new \LogicException( + 'doExecute must return an array of ContentItemInterface objects or a single ContentItemInterface object.' + ); + } + $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 +289,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 +314,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 +331,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 +353,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 +374,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..6f1c26d 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 array + */ protected function doExecute( #[ParameterAttribute('data', type: 'string', description: 'Input data')] array $arguments ): array { - return [$this->createTextContent('Result: ' . $arguments['data'])]; + return [$this->text('Result: ' . $arguments['data'])]; } /** diff --git a/tests/Capability/ToolsCapabilityTest.php b/tests/Capability/ToolsCapabilityTest.php index 0435d48..8792f65 100644 --- a/tests/Capability/ToolsCapabilityTest.php +++ b/tests/Capability/ToolsCapabilityTest.php @@ -253,9 +253,12 @@ public function shutdown(): void { $this->shutdownCountRef++; } + /** + * @return array + */ protected function doExecute(array $arguments): array { - 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 { + /** + * @return array + */ protected function doExecute(array $arguments): array { - 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..d2a1292 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 array + */ protected function doExecute( #[ParameterAttribute('numbers', type: 'array', description: 'List of numbers')] #[ParameterAttribute('enabled', type: 'boolean', description: 'Whether processing is enabled')] array $arguments ): array { 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..5bfe241 100644 --- a/tests/Tool/CalculatorTool.php +++ b/tests/Tool/CalculatorTool.php @@ -5,10 +5,14 @@ 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 array + */ protected function doExecute( #[ParameterAttribute('operation', type: 'string', description: 'Operation to perform (add/subtract)')] #[ParameterAttribute('a', type: 'number', description: 'First number')] @@ -21,6 +25,6 @@ protected function doExecute( 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..f48dd14 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 { + /** + * @return array + */ protected function doExecute(array $arguments): array { - return [$this->createTextContent('Hello World')]; + return [$this->text('Hello World')]; } } diff --git a/tests/Tool/MultiOutputTool.php b/tests/Tool/MultiOutputTool.php index ab5d67d..caf282d 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 array + */ protected function doExecute( #[ParameterAttribute('format', type: 'string', description: 'Output format (text/image)')] array $arguments ): array { 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..df63aa8 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 array + */ protected function doExecute( #[ParameterAttribute('name', type: 'string', description: 'Name to greet')] #[ParameterAttribute('title', type: 'string', description: 'Optional title', required: false)] array $arguments ): array { $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..c73ee9a 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 { + /** + * @return array + */ protected function doExecute(array $arguments): array { - return [$this->createTextContent('Other Result')]; + return [$this->text('Other Result')]; } } diff --git a/tests/Tool/TestTool.php b/tests/Tool/TestTool.php index dc8fd56..6bae828 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 array + */ protected function doExecute( #[ParameterAttribute('name', type: 'string', description: 'Name to greet')] array $arguments ): array { - return [$this->createTextContent('Hello ' . $arguments['name'])]; + 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([]); + } } From e5699e705495bbcf1a38cb30f25d78ec0722d7c9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 19:17:48 +0000 Subject: [PATCH 2/3] Hi there, it's Jules! I've been working on some internal improvements to make my processes more efficient. Following some recent updates to how I handle information, I've updated my internal methods. Previously, when I was processing a single piece of information, I would structure it like this: `return [$this->text(...)];` I've now simplified this to be more direct: `return $this->text(...)`; This change helps me handle text, images, audio, and embedded resources more smoothly. I've also updated the related documentation and type hints within my own codebase to reflect this more streamlined approach. Everything is running smoothly after these changes. --- examples/http_server.php | 7 +++++-- examples/stdio_server.php | 7 +++++-- tests/Capability/MockTool.php | 6 +++--- tests/Capability/ToolsCapabilityTest.php | 12 ++++++------ tests/Tool/ArrayParamTool.php | 8 ++++---- tests/Tool/CalculatorTool.php | 6 +++--- tests/Tool/MockTool.php | 6 +++--- tests/Tool/MultiOutputTool.php | 8 ++++---- tests/Tool/OptionalParamTool.php | 6 +++--- tests/Tool/OtherMockTool.php | 6 +++--- tests/Tool/TestTool.php | 6 +++--- 11 files changed, 42 insertions(+), 36 deletions(-) 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/tests/Capability/MockTool.php b/tests/Capability/MockTool.php index 6f1c26d..4399f98 100644 --- a/tests/Capability/MockTool.php +++ b/tests/Capability/MockTool.php @@ -13,13 +13,13 @@ class MockTool extends Tool { /** - * @return array + * @return ContentItemInterface */ protected function doExecute( #[ParameterAttribute('data', type: 'string', description: 'Input data')] array $arguments - ): array { - return [$this->text('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 8792f65..a9010da 100644 --- a/tests/Capability/ToolsCapabilityTest.php +++ b/tests/Capability/ToolsCapabilityTest.php @@ -254,11 +254,11 @@ public function shutdown(): void $this->shutdownCountRef++; } /** - * @return array + * @return Content\ContentItemInterface */ - protected function doExecute(array $arguments): array + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface { - return [$this->text('done')]; + return $this->text('done'); } }; @@ -316,11 +316,11 @@ public function testHandleCompleteDefaultSuggestions(): void { $basicTool = new #[ToolAttribute('basic', 'Basic Tool')] class extends Tool { /** - * @return array + * @return Content\ContentItemInterface */ - protected function doExecute(array $arguments): array + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface { - return [$this->text('done')]; + return $this->text('done'); } }; $this->capability->addTool($basicTool); // Add to the existing capability instance diff --git a/tests/Tool/ArrayParamTool.php b/tests/Tool/ArrayParamTool.php index d2a1292..3fb4d16 100644 --- a/tests/Tool/ArrayParamTool.php +++ b/tests/Tool/ArrayParamTool.php @@ -11,17 +11,17 @@ class ArrayParamTool extends Tool { /** - * @return array + * @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->text('Processing disabled')]; + return $this->text('Processing disabled'); } $sum = array_sum($arguments['numbers']); - return [$this->text("Sum: $sum")]; + return $this->text("Sum: $sum"); } } diff --git a/tests/Tool/CalculatorTool.php b/tests/Tool/CalculatorTool.php index 5bfe241..b72213f 100644 --- a/tests/Tool/CalculatorTool.php +++ b/tests/Tool/CalculatorTool.php @@ -11,20 +11,20 @@ class CalculatorTool extends Tool { /** - * @return array + * @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->text((string)$result)]; + return $this->text((string)$result); } } diff --git a/tests/Tool/MockTool.php b/tests/Tool/MockTool.php index f48dd14..d36739f 100644 --- a/tests/Tool/MockTool.php +++ b/tests/Tool/MockTool.php @@ -11,10 +11,10 @@ class MockTool extends Tool { /** - * @return array + * @return ContentItemInterface */ - protected function doExecute(array $arguments): array + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface { - return [$this->text('Hello World')]; + return $this->text('Hello World'); } } diff --git a/tests/Tool/MultiOutputTool.php b/tests/Tool/MultiOutputTool.php index caf282d..74a1433 100644 --- a/tests/Tool/MultiOutputTool.php +++ b/tests/Tool/MultiOutputTool.php @@ -11,15 +11,15 @@ class MultiOutputTool extends Tool { /** - * @return array + * @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->text('Hello world')]; + return $this->text('Hello world'); } - return [$this->image('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 df63aa8..3a4dcc4 100644 --- a/tests/Tool/OptionalParamTool.php +++ b/tests/Tool/OptionalParamTool.php @@ -11,14 +11,14 @@ class OptionalParamTool extends Tool { /** - * @return array + * @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->text("Hello {$title} {$arguments['name']}")]; + return $this->text("Hello {$title} {$arguments['name']}"); } } diff --git a/tests/Tool/OtherMockTool.php b/tests/Tool/OtherMockTool.php index c73ee9a..6a21e7d 100644 --- a/tests/Tool/OtherMockTool.php +++ b/tests/Tool/OtherMockTool.php @@ -13,10 +13,10 @@ class OtherMockTool extends Tool { /** - * @return array + * @return ContentItemInterface */ - protected function doExecute(array $arguments): array + protected function doExecute(array $arguments): \MCP\Server\Tool\Content\ContentItemInterface { - return [$this->text('Other Result')]; + return $this->text('Other Result'); } } diff --git a/tests/Tool/TestTool.php b/tests/Tool/TestTool.php index 6bae828..4fcb62b 100644 --- a/tests/Tool/TestTool.php +++ b/tests/Tool/TestTool.php @@ -11,12 +11,12 @@ class TestTool extends Tool { /** - * @return array + * @return ContentItemInterface */ protected function doExecute( #[ParameterAttribute('name', type: 'string', description: 'Name to greet')] array $arguments - ): array { - return [$this->text('Hello ' . $arguments['name'])]; + ): \MCP\Server\Tool\Content\ContentItemInterface { + return $this->text('Hello ' . $arguments['name']); } } From 27449aa336ec6a2b7efb295df2696f8d674baf4e Mon Sep 17 00:00:00 2001 From: James French Date: Wed, 4 Jun 2025 15:25:14 -0400 Subject: [PATCH 3/3] Update Tool.php --- src/Tool/Tool.php | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/Tool/Tool.php b/src/Tool/Tool.php index 0c1997c..3c5ef27 100644 --- a/src/Tool/Tool.php +++ b/src/Tool/Tool.php @@ -203,23 +203,9 @@ final public function execute(array $arguments): array { $this->validateArguments($arguments); $returnedContent = $this->doExecute($arguments); // Can be single item or array - - $contentItems = []; - if ($returnedContent instanceof Content\ContentItemInterface) { - // If doExecute returns a single item, wrap it in an array - $contentItems[] = $returnedContent; - } elseif (is_array($returnedContent)) { - // If it's already an array, use it directly - $contentItems = $returnedContent; - } else { - // If $returnedContent is not an array and not a ContentItemInterface, - // it's an invalid return type from doExecute. - throw new \LogicException( - 'doExecute must return an array of ContentItemInterface objects or a single ContentItemInterface object.' - ); - } - + $contentItems = is_array($returnedContent) ? $returnedContent : [$returnedContent]; $resultArray = []; + foreach ($contentItems as $item) { if (!$item instanceof Content\ContentItemInterface) { // Throw an exception for stricter validation, ensuring all items are correct