Skip to content

Commit 538549f

Browse files
committed
StructuredContent call tool result, added ability to return error from tool
1 parent 6100ffc commit 538549f

File tree

6 files changed

+182
-4
lines changed

6 files changed

+182
-4
lines changed

src/Schema/Result/CallToolResult.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use Mcp\Schema\Content\EmbeddedResource;
1818
use Mcp\Schema\Content\ImageContent;
1919
use Mcp\Schema\Content\TextContent;
20-
use Mcp\Schema\JsonRpc\ResultInterface;
2120

2221
/**
2322
* The server's response to a tool call.
@@ -33,7 +32,7 @@
3332
*
3433
* @author Kyrian Obikwelu <[email protected]>
3534
*/
36-
class CallToolResult implements ResultInterface
35+
class CallToolResult implements CallToolResultInterface
3736
{
3837
/**
3938
* Create a new CallToolResult.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Schema\Result;
13+
14+
use Mcp\Schema\JsonRpc\ResultInterface;
15+
16+
interface CallToolResultInterface extends ResultInterface
17+
{
18+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Schema\Result;
13+
14+
class CallToolStructuredContentResult implements CallToolResultInterface
15+
{
16+
/**
17+
* Creates a new CallToolResult with structured content.
18+
*
19+
* @see https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
20+
*
21+
* @param mixed[] $structuredContent JSON content for `structuredContent`
22+
* @param CallToolResultInterface $callToolResult Traditional result
23+
*/
24+
public function __construct(
25+
public readonly array $structuredContent,
26+
public readonly CallToolResultInterface $callToolResult,
27+
) {
28+
}
29+
30+
public function jsonSerialize(): mixed
31+
{
32+
return ['structuredContent' => $this->structuredContent] + $this->callToolResult->jsonSerialize();
33+
}
34+
}

src/Server/Handler/Request/CallToolHandler.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Mcp\Schema\JsonRpc\Response;
2222
use Mcp\Schema\Request\CallToolRequest;
2323
use Mcp\Schema\Result\CallToolResult;
24+
use Mcp\Schema\Result\CallToolResultInterface;
2425
use Mcp\Server\Session\SessionInterface;
2526
use Psr\Log\LoggerInterface;
2627
use Psr\Log\NullLogger;
@@ -59,14 +60,17 @@ public function handle(Request $request, SessionInterface $session): Response|Er
5960
}
6061

6162
$result = $this->referenceHandler->handle($reference, $arguments);
62-
$formatted = $reference->formatResult($result);
63+
64+
if (!$result instanceof CallToolResultInterface) {
65+
$result = new CallToolResult($reference->formatResult($result));
66+
}
6367

6468
$this->logger->debug('Tool executed successfully', [
6569
'name' => $toolName,
6670
'result_type' => \gettype($result),
6771
]);
6872

69-
return new Response($request->getId(), new CallToolResult($formatted));
73+
return new Response($request->getId(), $result);
7074
} catch (ToolNotFoundException $e) {
7175
$this->logger->error('Tool not found', ['name' => $toolName]);
7276

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Tests\Unit\Schema\Result;
13+
14+
use Mcp\Schema\Content\TextContent;
15+
use Mcp\Schema\Result\CallToolResult;
16+
use Mcp\Schema\Result\CallToolStructuredContentResult;
17+
use PHPUnit\Framework\TestCase;
18+
19+
class CallToolStructuredContentResultTest extends TestCase
20+
{
21+
/**
22+
* @dataProvider provideJsonSerializeScenarios
23+
*
24+
* @param array<string, mixed> $structuredContent
25+
* @param array<string, mixed> $expected
26+
*/
27+
public function testJsonSerialize(
28+
array $structuredContent,
29+
CallToolResult $callToolResult,
30+
array $expected,
31+
): void {
32+
$result = new CallToolStructuredContentResult($structuredContent, $callToolResult);
33+
34+
$this->assertEquals($expected, $result->jsonSerialize());
35+
}
36+
37+
/**
38+
* @return iterable<string, array{array<string, mixed>, CallToolResult, array<string, mixed>}>
39+
*/
40+
public static function provideJsonSerializeScenarios(): iterable
41+
{
42+
$successContent = new TextContent('Hello, World!');
43+
$errorContent = new TextContent('Failure details');
44+
45+
yield 'success' => [
46+
['result' => 'Hello, World!'],
47+
new CallToolResult([$successContent]),
48+
[
49+
'structuredContent' => ['result' => 'Hello, World!'],
50+
'content' => [$successContent],
51+
'isError' => false,
52+
],
53+
];
54+
55+
yield 'error' => [
56+
['result' => 'Failure details'],
57+
CallToolResult::error([$errorContent]),
58+
[
59+
'structuredContent' => ['result' => 'Failure details'],
60+
'content' => [$errorContent],
61+
'isError' => true,
62+
],
63+
];
64+
}
65+
}

tests/Unit/Server/Handler/Request/CallToolHandlerTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Mcp\Schema\JsonRpc\Response;
2222
use Mcp\Schema\Request\CallToolRequest;
2323
use Mcp\Schema\Result\CallToolResult;
24+
use Mcp\Schema\Result\CallToolStructuredContentResult;
2425
use Mcp\Server\Handler\Request\CallToolHandler;
2526
use Mcp\Server\Session\SessionInterface;
2627
use PHPUnit\Framework\MockObject\MockObject;
@@ -342,6 +343,63 @@ public function testHandleWithSpecialCharactersInArguments(): void
342343
$this->assertEquals($expectedResult, $response->result);
343344
}
344345

346+
public function testHandleReturnsStructuredContentResult(): void
347+
{
348+
$request = $this->createCallToolRequest('structured_tool', ['query' => 'php']);
349+
$toolReference = $this->createMock(ToolReference::class);
350+
$innerResult = new CallToolResult([new TextContent('Rendered results')]);
351+
$structuredResult = new CallToolStructuredContentResult(['result' => 'Rendered results'], $innerResult);
352+
353+
$this->referenceProvider
354+
->expects($this->once())
355+
->method('getTool')
356+
->with('structured_tool')
357+
->willReturn($toolReference);
358+
359+
$this->referenceHandler
360+
->expects($this->once())
361+
->method('handle')
362+
->with($toolReference, ['query' => 'php'])
363+
->willReturn($structuredResult);
364+
365+
$toolReference
366+
->expects($this->never())
367+
->method('formatResult');
368+
369+
$response = $this->handler->handle($request, $this->session);
370+
371+
$this->assertInstanceOf(Response::class, $response);
372+
$this->assertSame($structuredResult, $response->result);
373+
}
374+
375+
public function testHandleReturnsCallToolResult(): void
376+
{
377+
$request = $this->createCallToolRequest('result_tool', ['query' => 'php']);
378+
$toolReference = $this->createMock(ToolReference::class);
379+
$callToolResult = new CallToolResult([new TextContent('Rendered results')]);
380+
381+
$this->referenceProvider
382+
->expects($this->once())
383+
->method('getTool')
384+
->with('result_tool')
385+
->willReturn($toolReference);
386+
387+
$this->referenceHandler
388+
->expects($this->once())
389+
->method('handle')
390+
->with($toolReference, ['query' => 'php'])
391+
->willReturn($callToolResult);
392+
393+
$toolReference
394+
->expects($this->never())
395+
->method('formatResult');
396+
397+
$response = $this->handler->handle($request, $this->session);
398+
399+
$this->assertInstanceOf(Response::class, $response);
400+
$this->assertSame($callToolResult, $response->result);
401+
}
402+
345403
/**
346404
* @param array<string, mixed> $arguments
347405
*/

0 commit comments

Comments
 (0)