From 93dd2db892ef6cac8b45b4d0e2e4e5fbeda4021c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 5 Jan 2026 21:52:47 +0100 Subject: [PATCH 01/11] Implement io.modelcontextprotocol.server.Result class --- .../modelcontextprotocol/McpServer.class.php | 8 +- .../server/Result.class.php | 91 +++++++++++++++++++ .../McpServerToolCallingTest.class.php | 18 +++- .../unittest/ResultTest.class.php | 69 ++++++++++++++ 4 files changed, 179 insertions(+), 7 deletions(-) create mode 100755 src/main/php/io/modelcontextprotocol/server/Result.class.php create mode 100755 src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php diff --git a/src/main/php/io/modelcontextprotocol/McpServer.class.php b/src/main/php/io/modelcontextprotocol/McpServer.class.php index 5c52b1e..76ba6e3 100755 --- a/src/main/php/io/modelcontextprotocol/McpServer.class.php +++ b/src/main/php/io/modelcontextprotocol/McpServer.class.php @@ -1,6 +1,6 @@ function($payload, $request) { if ($invokeable= $this->delegate->invokeable($payload['params']['name'])) { - $result= $invokeable((array)$payload['params']['arguments'], $request); - return ['content' => [['type' => 'text', 'text' => is_string($result) - ? $result - : Json::of($result) - ]]]; + return Result::cast($invokeable((array)$payload['params']['arguments'], $request))->struct(); } throw new NoSuchElementException($payload['params']['name']); }, diff --git a/src/main/php/io/modelcontextprotocol/server/Result.class.php b/src/main/php/io/modelcontextprotocol/server/Result.class.php new file mode 100755 index 0000000..3e3b603 --- /dev/null +++ b/src/main/php/io/modelcontextprotocol/server/Result.class.php @@ -0,0 +1,91 @@ +struct= $struct; } + + /** Returns the structure */ + public function struct(): array { return $this->struct; } + + /** Creates a result from a given value */ + public static function cast($value): self { + if ($value instanceof self) { + return $value; + } else if (is_scalar($value)) { + return self::success()->text($value); + } else { + return self::structured($value); + } + } + + /** Creates a success result */ + public static function success(): self { + return new self(['content' => []]); + } + + /** Creates an error result */ + public static function error(): self { + return new self(['content' => [], 'isError' => true]); + } + + /** Creates an special unstructured result including a textual JSON-encoded representation */ + public static function structured(array $object): self { + return new self([ + 'structuredContent' => $object, + 'content' => [['type' => 'text', 'text' => json_encode($object)]], + ]); + } + + /** Adds a given typed content */ + public function add(string $type, array $struct, array $annotations= []): self { + $this->struct['content'][]= [ + 'type' => $type, + ...$struct, + ...($annotations ? ['annotations' => $annotations] : []) + ]; + return $this; + } + + /** + * Adds a text content + * + * @param string $string + * @param [:mixed] $annotations + */ + public function text($string, $annotations= []): self { + return $this->add('text', ['text' => (string)$string], $annotations); + } + + /** + * Adds an image content + * + * @param string|util.Bytes $data + * @param string $mime + * @param [:mixed] $annotations + */ + public function image($data, $mime, $annotations= []): self { + return $this->add('image', ['data' => base64_encode($data), 'mimeType' => $mime], $annotations); + } + + /** + * Adds an audio content + * + * @param string|util.Bytes $data + * @param string $mime + * @param [:mixed] $annotations + */ + public function audio($data, $mime, $annotations= []): self { + return $this->add('audio', ['data' => base64_encode($data), 'mimeType' => $mime], $annotations); + } +} \ No newline at end of file diff --git a/src/test/php/io/modelcontextprotocol/unittest/McpServerToolCallingTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/McpServerToolCallingTest.class.php index ac63d20..9692130 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/McpServerToolCallingTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/McpServerToolCallingTest.class.php @@ -1,6 +1,6 @@ method('tools/call', ['name' => 'test_fixture', 'arguments' => []], new class() { + + #[Tool] + public function fixture() { + return Result::success()->text('Hi', ['audience' => ['user']]); + } + }); + + Assert::equals( + '{"jsonrpc":"2.0","id":"1","result":{"content":[{"type":"text","text":"Hi","annotations":{"audience":["user"]}}]}}', + $answer + ); + } + #[Test] public function tool_raising_error() { $answer= $this->method('tools/call', ['name' => 'test_fixture', 'arguments' => []], new class() { diff --git a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php new file mode 100755 index 0000000..c3691a4 --- /dev/null +++ b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php @@ -0,0 +1,69 @@ + []], Result::success()->struct()); + } + + #[Test] + public function error() { + Assert::equals(['content' => [], 'isError' => true], Result::error()->struct()); + } + + #[Test] + public function add() { + Assert::equals( + ['content' => [['type' => 'text', 'text' => 'Test']]], + Result::success()->add('text', ['text' => 'Test'])->struct() + ); + } + + #[Test] + public function annotations() { + Assert::equals( + ['content' => [['type' => 'text', 'text' => 'Test','annotations' => ['audience' => ['user']]]]], + Result::success()->add('text', ['text' => 'Test'], ['audience' => ['user']])->struct() + ); + } + + #[Test] + public function with_text() { + Assert::equals( + ['content' => [['type' => 'text', 'text' => 'Tool result text']]], + Result::success()->text('Tool result text')->struct() + ); + } + + #[Test] + public function with_image() { + Assert::equals( + ['content' => [['type' => 'image', 'data' => 'R0lGODlhLi4u', 'mimeType' => 'image/gif']]], + Result::success()->image('GIF89a...', 'image/gif')->struct() + ); + } + + #[Test] + public function with_audio() { + Assert::equals( + ['content' => [['type' => 'audio', 'data' => 'UklGRi4uLg==', 'mimeType' => 'audio/wav']]], + Result::success()->audio('RIFF...', 'audio/wav')->struct() + ); + } + + #[Test] + public function structured() { + $object= ['temperature' => 22.5, 'conditions' => 'Partly cloudy', 'humidity' => 65]; + Assert::equals( + [ + 'structuredContent' => $object, + 'content' => [['type' => 'text', 'text' => json_encode($object)]], + ], + Result::structured($object)->struct() + ); + } +} \ No newline at end of file From b6e79385eef6640b359b002493f049654ba291c9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 5 Jan 2026 21:59:26 +0100 Subject: [PATCH 02/11] Fix "Cannot unpack array with string keys" (PHP 7.4 & 8.0) --- .../php/io/modelcontextprotocol/server/Result.class.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/php/io/modelcontextprotocol/server/Result.class.php b/src/main/php/io/modelcontextprotocol/server/Result.class.php index 3e3b603..0e9f09d 100755 --- a/src/main/php/io/modelcontextprotocol/server/Result.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Result.class.php @@ -49,11 +49,10 @@ public static function structured(array $object): self { /** Adds a given typed content */ public function add(string $type, array $struct, array $annotations= []): self { - $this->struct['content'][]= [ - 'type' => $type, - ...$struct, - ...($annotations ? ['annotations' => $annotations] : []) - ]; + $this->struct['content'][]= ['type' => $type] + $struct + ($annotations + ? ['annotations' => $annotations] + : [] + ); return $this; } From efd5a54eae5f95c163e219533b6a47204d243c01 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 5 Jan 2026 22:09:12 +0100 Subject: [PATCH 03/11] Implement resource links --- .../server/Result.class.php | 17 +++++++++++++++++ .../unittest/ResultTest.class.php | 14 ++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/main/php/io/modelcontextprotocol/server/Result.class.php b/src/main/php/io/modelcontextprotocol/server/Result.class.php index 0e9f09d..6aeaf57 100755 --- a/src/main/php/io/modelcontextprotocol/server/Result.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Result.class.php @@ -87,4 +87,21 @@ public function image($data, $mime, $annotations= []): self { public function audio($data, $mime, $annotations= []): self { return $this->add('audio', ['data' => base64_encode($data), 'mimeType' => $mime], $annotations); } + + /** + * Adds a resource link + * + * @param string $uri + * @param string $name + * @param string $description + * @param string $mime + * @param [:mixed] $annotations + */ + public function link($uri, $name, $description, $mime, $annotations= []): self { + return $this->add( + 'resource_link', + ['uri' => $uri, 'name' => $name, 'description' => $description, 'mimeType' => $mime], + $annotations + ); + } } \ No newline at end of file diff --git a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php index c3691a4..0c804ee 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php @@ -55,6 +55,20 @@ public function with_audio() { ); } + #[Test] + public function with_link() { + Assert::equals( + ['content' => [[ + 'type' => 'resource_link', + 'uri' => 'file:///project/src/main.rs', + 'name' => 'main.rs', + 'description' => 'Main', + 'mimeType' => 'text/x-rust', + ]]], + Result::success()->link('file:///project/src/main.rs', 'main.rs', 'Main', 'text/x-rust')->struct() + ); + } + #[Test] public function structured() { $object= ['temperature' => 22.5, 'conditions' => 'Partly cloudy', 'humidity' => 65]; From baffde36849992cf390653ebd9178a46d02e52df Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 5 Jan 2026 22:14:32 +0100 Subject: [PATCH 04/11] Implement embedded resources --- .../server/Result.class.php | 16 ++++++++++++++++ .../unittest/ResultTest.class.php | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/main/php/io/modelcontextprotocol/server/Result.class.php b/src/main/php/io/modelcontextprotocol/server/Result.class.php index 6aeaf57..17c0002 100755 --- a/src/main/php/io/modelcontextprotocol/server/Result.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Result.class.php @@ -104,4 +104,20 @@ public function link($uri, $name, $description, $mime, $annotations= []): self { $annotations ); } + + /** + * Adds an embedded resource + * + * @param string $uri + * @param string $mime + * @param string|util.Bytes $text + * @param [:mixed] $annotations + */ + public function resource($uri, $mime, $text, $annotations= []): self { + return $this->add( + 'resource', + ['resource' => ['uri' => $uri, 'mimeType' => $mime, 'text' => (string)$text]], + $annotations + ); + } } \ No newline at end of file diff --git a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php index 0c804ee..f1cb55b 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php @@ -56,7 +56,7 @@ public function with_audio() { } #[Test] - public function with_link() { + public function with_resource_link() { Assert::equals( ['content' => [[ 'type' => 'resource_link', @@ -69,6 +69,22 @@ public function with_link() { ); } + #[Test] + public function with_embedded_resource() { + $code= "fn main() {\n println!(\"Hello world!\");\n}"; + Assert::equals( + ['content' => [[ + 'type' => 'resource', + 'resource' => [ + 'uri' => 'file:///project/src/main.rs', + 'mimeType' => 'text/x-rust', + 'text' => $code, + ], + ]]], + Result::success()->resource('file:///project/src/main.rs', 'text/x-rust', $code)->struct() + ); + } + #[Test] public function structured() { $object= ['temperature' => 22.5, 'conditions' => 'Partly cloudy', 'humidity' => 65]; From 6678d819ca3fd25b4e5060409e23d6883e08d86b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 5 Jan 2026 22:17:34 +0100 Subject: [PATCH 05/11] Add tests for cast() --- .../unittest/ResultTest.class.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php index f1cb55b..43cd9f9 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php @@ -96,4 +96,24 @@ public function structured() { Result::structured($object)->struct() ); } + + #[Test] + public function cast_scalar() { + Assert::equals( + ['content' => [['type' => 'text', 'text' => 'Test']]], + Result::success()->cast('Test')->struct() + ); + } + + #[Test] + public function cast_object() { + $object= ['temperature' => 22.5, 'conditions' => 'Partly cloudy', 'humidity' => 65]; + Assert::equals( + [ + 'structuredContent' => $object, + 'content' => [['type' => 'text', 'text' => json_encode($object)]], + ], + Result::success()->cast($object)->struct() + ); + } } \ No newline at end of file From 9e5a55824b044c9c7278d59883e5d75c67542bf5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 5 Jan 2026 22:51:26 +0100 Subject: [PATCH 06/11] Add optional value parameter to success() and error() --- .../server/Result.class.php | 28 +++++++++++++------ .../unittest/ResultTest.class.php | 16 +++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/main/php/io/modelcontextprotocol/server/Result.class.php b/src/main/php/io/modelcontextprotocol/server/Result.class.php index 17c0002..5746224 100755 --- a/src/main/php/io/modelcontextprotocol/server/Result.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Result.class.php @@ -18,29 +18,41 @@ public function __construct(array $struct) { $this->struct= $struct; } /** Returns the structure */ public function struct(): array { return $this->struct; } + /** Maps a value to result */ + private static function of($value) { + if (null === $value) { + return ['content' => []]; + } else if (is_scalar($value)) { + return ['content' => [['type' => 'text', 'text' => (string)$value]]]; + } else { + return [ + 'structuredContent' => $value, + 'content' => [['type' => 'text', 'text' => json_encode($value)]], + ]; + } + } + /** Creates a result from a given value */ public static function cast($value): self { if ($value instanceof self) { return $value; - } else if (is_scalar($value)) { - return self::success()->text($value); } else { - return self::structured($value); + return new self(self::of($value)); } } /** Creates a success result */ - public static function success(): self { - return new self(['content' => []]); + public static function success($value= null): self { + return new self(self::of($value)); } /** Creates an error result */ - public static function error(): self { - return new self(['content' => [], 'isError' => true]); + public static function error($value= null): self { + return new self(self::of($value) + ['isError' => true]); } /** Creates an special unstructured result including a textual JSON-encoded representation */ - public static function structured(array $object): self { + public static function structured($object): self { return new self([ 'structuredContent' => $object, 'content' => [['type' => 'text', 'text' => json_encode($object)]], diff --git a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php index 43cd9f9..ae9af78 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php @@ -15,6 +15,22 @@ public function error() { Assert::equals(['content' => [], 'isError' => true], Result::error()->struct()); } + #[Test] + public function success_with_text() { + Assert::equals( + ['content' => [['type' => 'text', 'text' => 'It worked']]], + Result::success('It worked')->struct() + ); + } + + #[Test] + public function error_with_text() { + Assert::equals( + ['content' => [['type' => 'text', 'text' => 'Error 404']], 'isError' => true], + Result::error('Error 404')->struct() + ); + } + #[Test] public function add() { Assert::equals( From bb1940c3b99eb344462c54ba4d75d056b8b5f3e4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 5 Jan 2026 22:54:55 +0100 Subject: [PATCH 07/11] Reorder struct() to end --- .../php/io/modelcontextprotocol/server/Result.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/io/modelcontextprotocol/server/Result.class.php b/src/main/php/io/modelcontextprotocol/server/Result.class.php index 5746224..68908ac 100755 --- a/src/main/php/io/modelcontextprotocol/server/Result.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Result.class.php @@ -15,9 +15,6 @@ class Result { /** Creates a new result with the given structure */ public function __construct(array $struct) { $this->struct= $struct; } - /** Returns the structure */ - public function struct(): array { return $this->struct; } - /** Maps a value to result */ private static function of($value) { if (null === $value) { @@ -132,4 +129,7 @@ public function resource($uri, $mime, $text, $annotations= []): self { $annotations ); } + + /** Returns the structure */ + public function struct(): array { return $this->struct; } } \ No newline at end of file From 13e110b9b6417ce59109bee28a9c986f216cf736 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Jan 2026 09:08:30 +0100 Subject: [PATCH 08/11] Make resource() consistent with image() and audio() Parameter for the latter order is , ; and was , for embedded resources as shown in the specification example. We vote for API consistency here --- .../php/io/modelcontextprotocol/server/Result.class.php | 6 +++--- .../io/modelcontextprotocol/unittest/ResultTest.class.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/php/io/modelcontextprotocol/server/Result.class.php b/src/main/php/io/modelcontextprotocol/server/Result.class.php index 68908ac..5fd57e7 100755 --- a/src/main/php/io/modelcontextprotocol/server/Result.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Result.class.php @@ -118,14 +118,14 @@ public function link($uri, $name, $description, $mime, $annotations= []): self { * Adds an embedded resource * * @param string $uri - * @param string $mime * @param string|util.Bytes $text + * @param string $mime * @param [:mixed] $annotations */ - public function resource($uri, $mime, $text, $annotations= []): self { + public function resource($uri, $text, $mime, $annotations= []): self { return $this->add( 'resource', - ['resource' => ['uri' => $uri, 'mimeType' => $mime, 'text' => (string)$text]], + ['resource' => ['uri' => $uri, 'text' => (string)$text, 'mimeType' => $mime]], $annotations ); } diff --git a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php index ae9af78..c288fd9 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php @@ -93,11 +93,11 @@ public function with_embedded_resource() { 'type' => 'resource', 'resource' => [ 'uri' => 'file:///project/src/main.rs', - 'mimeType' => 'text/x-rust', 'text' => $code, + 'mimeType' => 'text/x-rust', ], ]]], - Result::success()->resource('file:///project/src/main.rs', 'text/x-rust', $code)->struct() + Result::success()->resource('file:///project/src/main.rs', $code, 'text/x-rust')->struct() ); } From bb52ec675ecec0da87cacd138daf26b37a7c061f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Jan 2026 09:22:50 +0100 Subject: [PATCH 09/11] Add tests for passing objects to success() and error() --- .../unittest/ResultTest.class.php | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php index c288fd9..dc1a6bf 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php @@ -4,17 +4,13 @@ use test\{Assert, Test}; class ResultTest { + const OBJECT= ['temperature' => 22.5, 'conditions' => 'Partly cloudy', 'humidity' => 65]; #[Test] public function success() { Assert::equals(['content' => []], Result::success()->struct()); } - #[Test] - public function error() { - Assert::equals(['content' => [], 'isError' => true], Result::error()->struct()); - } - #[Test] public function success_with_text() { Assert::equals( @@ -23,6 +19,22 @@ public function success_with_text() { ); } + #[Test] + public function success_with_object() { + Assert::equals( + [ + 'structuredContent' => self::OBJECT, + 'content' => [['type' => 'text', 'text' => json_encode(self::OBJECT)]], + ], + Result::success(self::OBJECT)->struct() + ); + } + + #[Test] + public function error() { + Assert::equals(['content' => [], 'isError' => true], Result::error()->struct()); + } + #[Test] public function error_with_text() { Assert::equals( @@ -31,6 +43,18 @@ public function error_with_text() { ); } + #[Test] + public function error_with_object() { + Assert::equals( + [ + 'structuredContent' => self::OBJECT, + 'content' => [['type' => 'text', 'text' => json_encode(self::OBJECT)]], + 'isError' => true, + ], + Result::error(self::OBJECT)->struct() + ); + } + #[Test] public function add() { Assert::equals( @@ -103,13 +127,12 @@ public function with_embedded_resource() { #[Test] public function structured() { - $object= ['temperature' => 22.5, 'conditions' => 'Partly cloudy', 'humidity' => 65]; Assert::equals( [ - 'structuredContent' => $object, - 'content' => [['type' => 'text', 'text' => json_encode($object)]], + 'structuredContent' => self::OBJECT, + 'content' => [['type' => 'text', 'text' => json_encode(self::OBJECT)]], ], - Result::structured($object)->struct() + Result::structured(self::OBJECT)->struct() ); } @@ -123,13 +146,12 @@ public function cast_scalar() { #[Test] public function cast_object() { - $object= ['temperature' => 22.5, 'conditions' => 'Partly cloudy', 'humidity' => 65]; Assert::equals( [ - 'structuredContent' => $object, - 'content' => [['type' => 'text', 'text' => json_encode($object)]], + 'structuredContent' => self::OBJECT, + 'content' => [['type' => 'text', 'text' => json_encode(self::OBJECT)]], ], - Result::success()->cast($object)->struct() + Result::success()->cast(self::OBJECT)->struct() ); } } \ No newline at end of file From bbc96ec43a463353f48eaa2e159d44692c17f00f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Jan 2026 09:59:44 +0100 Subject: [PATCH 10/11] Only produce a JSON serialized version for objects in structured() The spec says: "SHOULD also return" (not: "MUST"). While the default implementation will omit this for performance reasons, tools may chose to use the structured($object) with either no additional parameter (which will produce the JSON) or an alternative textual representation given either a string or an iterable of strings, which will add text content parts --- .../server/Result.class.php | 38 +++++++++------- .../unittest/ResultTest.class.php | 44 +++++++++++++------ 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/main/php/io/modelcontextprotocol/server/Result.class.php b/src/main/php/io/modelcontextprotocol/server/Result.class.php index 5fd57e7..2c347f7 100755 --- a/src/main/php/io/modelcontextprotocol/server/Result.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Result.class.php @@ -22,20 +22,13 @@ private static function of($value) { } else if (is_scalar($value)) { return ['content' => [['type' => 'text', 'text' => (string)$value]]]; } else { - return [ - 'structuredContent' => $value, - 'content' => [['type' => 'text', 'text' => json_encode($value)]], - ]; + return ['structuredContent' => $value]; } } /** Creates a result from a given value */ public static function cast($value): self { - if ($value instanceof self) { - return $value; - } else { - return new self(self::of($value)); - } + return $value instanceof self ? $value : new self(self::of($value)); } /** Creates a success result */ @@ -48,12 +41,27 @@ public static function error($value= null): self { return new self(self::of($value) + ['isError' => true]); } - /** Creates an special unstructured result including a textual JSON-encoded representation */ - public static function structured($object): self { - return new self([ - 'structuredContent' => $object, - 'content' => [['type' => 'text', 'text' => json_encode($object)]], - ]); + /** + * Creates an special structured result including a textual representation, + * which defaults to a JSON-serialized version of the given object. + * + * @param var $object + * @param ?string|iterable $text + */ + public static function structured($object, $text= null): self { + $self= new self(['content' => [], 'structuredContent' => $object]); + + if (null === $text) { + $self->text(json_encode($object)); + } else if (is_iterable($text)) { + foreach ($text as $part) { + $self->text($part); + } + } else { + $self->text($text); + } + + return $self; } /** Adds a given typed content */ diff --git a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php index dc1a6bf..0cb6373 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php @@ -22,10 +22,7 @@ public function success_with_text() { #[Test] public function success_with_object() { Assert::equals( - [ - 'structuredContent' => self::OBJECT, - 'content' => [['type' => 'text', 'text' => json_encode(self::OBJECT)]], - ], + ['structuredContent' => self::OBJECT], Result::success(self::OBJECT)->struct() ); } @@ -46,11 +43,7 @@ public function error_with_text() { #[Test] public function error_with_object() { Assert::equals( - [ - 'structuredContent' => self::OBJECT, - 'content' => [['type' => 'text', 'text' => json_encode(self::OBJECT)]], - 'isError' => true, - ], + ['structuredContent' => self::OBJECT, 'isError' => true], Result::error(self::OBJECT)->struct() ); } @@ -136,6 +129,34 @@ public function structured() { ); } + #[Test] + public function structured_with_text() { + $text= 'Temperature: 22.5°, partly cloudy with a humidity of 65'; + Assert::equals( + [ + 'structuredContent' => self::OBJECT, + 'content' => [['type' => 'text', 'text' => $text]], + ], + Result::structured(self::OBJECT, $text)->struct() + ); + } + + #[Test] + public function structured_with_iterable() { + $text= ['Temperature: 22.5°', 'Conditions: partly cloudy', 'Humidity: 65']; + Assert::equals( + [ + 'content' => [ + ['type' => 'text', 'text' => $text[0]], + ['type' => 'text', 'text' => $text[1]], + ['type' => 'text', 'text' => $text[2]], + ], + 'structuredContent' => self::OBJECT, + ], + Result::structured(self::OBJECT, $text)->struct() + ); + } + #[Test] public function cast_scalar() { Assert::equals( @@ -147,10 +168,7 @@ public function cast_scalar() { #[Test] public function cast_object() { Assert::equals( - [ - 'structuredContent' => self::OBJECT, - 'content' => [['type' => 'text', 'text' => json_encode(self::OBJECT)]], - ], + ['structuredContent' => self::OBJECT], Result::success()->cast(self::OBJECT)->struct() ); } From 4894fe1d26028cd5dcdad9da090a80653bb482f1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Jan 2026 10:05:54 +0100 Subject: [PATCH 11/11] Add isError flag to structured(), enabling structured error representations --- .../io/modelcontextprotocol/server/Result.class.php | 4 +++- .../unittest/ResultTest.class.php | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/php/io/modelcontextprotocol/server/Result.class.php b/src/main/php/io/modelcontextprotocol/server/Result.class.php index 2c347f7..44eb23a 100755 --- a/src/main/php/io/modelcontextprotocol/server/Result.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Result.class.php @@ -47,8 +47,9 @@ public static function error($value= null): self { * * @param var $object * @param ?string|iterable $text + * @param ?bool $isError */ - public static function structured($object, $text= null): self { + public static function structured($object, $text= null, $isError= null): self { $self= new self(['content' => [], 'structuredContent' => $object]); if (null === $text) { @@ -61,6 +62,7 @@ public static function structured($object, $text= null): self { $self->text($text); } + isset($isError) && $self->struct['isError']= (bool)$isError; return $self; } diff --git a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php index 0cb6373..3aa50f6 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/ResultTest.class.php @@ -157,6 +157,19 @@ public function structured_with_iterable() { ); } + #[Test] + public function structured_with_error() { + $error= ['code' => 'INVALID_DEPARTURE_DATE', 'message' => 'Departure date must be in the future']; + Assert::equals( + [ + 'structuredContent' => ['error' => $error], + 'content' => [['type' => 'text', 'text' => $error['message']]], + 'isError' => true, + ], + Result::structured(['error' => $error], $error['message'], true)->struct() + ); + } + #[Test] public function cast_scalar() { Assert::equals(