Skip to content

Commit 7184352

Browse files
author
klapaudius
committed
Add ProfileGeneratorTool and CollectionToolResult with updated tests
- Added `CollectionToolResult` to support multiple result types within a single response. - Introduced `ProfileGeneratorTool` to illustrate CollectionToolResult with text and image. - Updated `klp_mcp_server.yaml` to register `ProfileGeneratorTool` and `StreamingDataTool`. - Enhanced `ToolsCallHandler` to handle `CollectionToolResult` outputs. - Added comprehensive tests for `ProfileGeneratorTool` and `CollectionToolResult` to verify functionality and edge cases.
1 parent 59dc7a7 commit 7184352

File tree

9 files changed

+493
-5
lines changed

9 files changed

+493
-5
lines changed

docs/assets/avatar_sample.jpg

9.18 KB
Loading

src/Resources/config/packages/klp_mcp_server.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ klp_mcp_server:
6969
# Register your tools here
7070
tools:
7171
- KLP\KlpMcpServer\Services\ToolService\Examples\HelloWorldTool
72+
- KLP\KlpMcpServer\Services\ToolService\Examples\ProfileGeneratorTool
73+
- KLP\KlpMcpServer\Services\ToolService\Examples\StreamingDataTool
7274
- KLP\KlpMcpServer\Services\ToolService\Examples\VersionCheckTool
7375

7476
# Prompts List

src/Server/Request/ToolsCallHandler.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use KLP\KlpMcpServer\Protocol\Handlers\RequestHandler;
99
use KLP\KlpMcpServer\Services\ProgressService\ProgressNotifierRepository;
1010
use KLP\KlpMcpServer\Services\ToolService\Result\AudioToolResult;
11+
use KLP\KlpMcpServer\Services\ToolService\Result\CollectionToolResult;
1112
use KLP\KlpMcpServer\Services\ToolService\Result\ImageToolResult;
1213
use KLP\KlpMcpServer\Services\ToolService\Result\ResourceToolResult;
1314
use KLP\KlpMcpServer\Services\ToolService\Result\TextToolResult;
@@ -92,11 +93,12 @@ public function execute(string $method, string $clientId, string|int $messageId,
9293

9394
$result = new TextToolResult(is_string($result) ? $result : json_encode($result));
9495
}
96+
$content = $result instanceof CollectionToolResult
97+
? $result->getSanitizedResult()
98+
: [ $result->getSanitizedResult() ];
9599

96100
return [
97-
'content' => [
98-
$result->getSanitizedResult(),
99-
],
101+
'content' => $content,
100102
];
101103
} else {
102104
return [
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
namespace KLP\KlpMcpServer\Services\ToolService\Examples;
4+
5+
use KLP\KlpMcpServer\Services\ProgressService\ProgressNotifierInterface;
6+
use KLP\KlpMcpServer\Services\ToolService\Annotation\ToolAnnotation;
7+
use KLP\KlpMcpServer\Services\ToolService\StreamableToolInterface;
8+
use KLP\KlpMcpServer\Services\ToolService\Result\CollectionToolResult;
9+
use KLP\KlpMcpServer\Services\ToolService\Result\ImageToolResult;
10+
use KLP\KlpMcpServer\Services\ToolService\Result\TextToolResult;
11+
use KLP\KlpMcpServer\Services\ToolService\Result\ToolResultInterface;
12+
use Symfony\Component\HttpKernel\KernelInterface;
13+
14+
/**
15+
* Example streaming tool that generates a user profile with text and image.
16+
*
17+
* This tool demonstrates how to return multiple result types using CollectionToolResult.
18+
*/
19+
class ProfileGeneratorTool implements StreamableToolInterface
20+
{
21+
private string $baseDir;
22+
private ProgressNotifierInterface $progressNotifier;
23+
24+
25+
public function __construct(KernelInterface $kernel)
26+
{
27+
$this->baseDir = $kernel->getProjectDir().'/vendor/klapaudius/symfony-mcp-server/docs';
28+
}
29+
public function getName(): string
30+
{
31+
return 'profile-generator';
32+
}
33+
34+
public function getDescription(): string
35+
{
36+
return 'Generates a user profile with text description and avatar image';
37+
}
38+
39+
public function getInputSchema(): array
40+
{
41+
return [
42+
'type' => 'object',
43+
'properties' => [
44+
'name' => [
45+
'type' => 'string',
46+
'description' => 'The name of the user',
47+
],
48+
'role' => [
49+
'type' => 'string',
50+
'description' => 'The role or profession of the user',
51+
],
52+
],
53+
'required' => ['name', 'role'],
54+
];
55+
}
56+
57+
public function getAnnotations(): ToolAnnotation
58+
{
59+
return new ToolAnnotation();
60+
}
61+
62+
public function execute(array $arguments): ToolResultInterface
63+
{
64+
$name = $arguments['name'] ?? 'Unknown User';
65+
$role = $arguments['role'] ?? 'User';
66+
67+
$collection = new CollectionToolResult();
68+
69+
// Generate text profile
70+
$this->progressNotifier->sendProgress(
71+
progress: 1,
72+
total: 3,
73+
message: 'Generating text profile...'
74+
);
75+
$profileText = $this->generateProfileText($name, $role);
76+
$collection->addItem(new TextToolResult($profileText));
77+
usleep(100000);
78+
79+
// Avatar image
80+
$this->progressNotifier->sendProgress(
81+
progress: 2,
82+
total: 3,
83+
message: 'Generating avatar image...'
84+
);
85+
$avatarImageData = base64_encode(file_get_contents($this->baseDir.'/assets/avatar_sample.jpg'));
86+
$collection->addItem(new ImageToolResult($avatarImageData, 'image/jpeg'));
87+
usleep(400000);
88+
$this->progressNotifier->sendProgress(
89+
progress: 3,
90+
total: 3,
91+
message: 'Done.'
92+
);
93+
94+
return $collection;
95+
}
96+
97+
public function isStreaming(): bool
98+
{
99+
return true;
100+
}
101+
102+
public function setProgressNotifier(ProgressNotifierInterface $progressNotifier): void
103+
{
104+
$this->progressNotifier = $progressNotifier;
105+
}
106+
107+
/**
108+
* Generates a text description for the user profile.
109+
*/
110+
private function generateProfileText(string $name, string $role): string
111+
{
112+
$createdAt = date('Y-m-d H:i:s');
113+
114+
return <<<TEXT
115+
=== User Profile ===
116+
Name: {$name}
117+
Role: {$role}
118+
Profile Created: {$createdAt}
119+
120+
Welcome, {$name}! As a {$role}, you're part of our growing community.
121+
Your profile has been successfully generated with a custom avatar.
122+
123+
Profile ID: {$this->generateProfileId($name)}
124+
Status: Active
125+
TEXT;
126+
}
127+
128+
/**
129+
* Generates a unique profile ID based on the name.
130+
*/
131+
private function generateProfileId(string $name): string
132+
{
133+
return 'PROF-' . strtoupper(substr(md5($name . time()), 0, 8));
134+
}
135+
}

src/Services/ToolService/Result/AbstractToolResult.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ protected function setValue(string $value): void
9090
/**
9191
* Returns the sanitized result array formatted according to MCP specification.
9292
*
93-
* @return array<string, mixed> The sanitized result data
93+
* @return array The sanitized result data
9494
*/
9595
abstract public function getSanitizedResult(): array;
9696
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace KLP\KlpMcpServer\Services\ToolService\Result;
4+
5+
/**
6+
* Represents a text result from a tool operation.
7+
*
8+
* This class encapsulates plain text content returned by a tool.
9+
* It is the most common type of tool result for simple text responses.
10+
*/
11+
final class CollectionToolResult extends AbstractToolResult
12+
{
13+
private array $items = [];
14+
15+
public function addItem(ToolResultInterface $item): void
16+
{
17+
if ($item instanceof CollectionToolResult) {
18+
throw new \InvalidArgumentException('CollectionToolResult cannot contain other CollectionToolResult.');
19+
}
20+
$this->items[] = $item;
21+
}
22+
23+
/**
24+
* {@inheritDoc}
25+
*/
26+
public function getSanitizedResult(): array
27+
{
28+
$result = [];
29+
foreach ($this->items as $item) {
30+
$result[] = $item->getSanitizedResult();
31+
}
32+
33+
return $result;
34+
}
35+
}

src/Services/ToolService/Result/ToolResultInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ interface ToolResultInterface
1414
* This method must return a properly formatted array that conforms to the
1515
* Model Context Protocol specification for tool results.
1616
*
17-
* @return array<string, mixed> The sanitized result data
17+
* @return array The sanitized result data
1818
*/
1919
public function getSanitizedResult(): array;
2020
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
namespace KLP\KlpMcpServer\Tests\Services\ToolService\Examples;
4+
5+
use KLP\KlpMcpServer\Services\ProgressService\ProgressNotifierInterface;
6+
use KLP\KlpMcpServer\Services\ToolService\Annotation\ToolAnnotation;
7+
use KLP\KlpMcpServer\Services\ToolService\Examples\ProfileGeneratorTool;
8+
use KLP\KlpMcpServer\Services\ToolService\Result\CollectionToolResult;
9+
use KLP\KlpMcpServer\Services\ToolService\Result\ImageToolResult;
10+
use KLP\KlpMcpServer\Services\ToolService\Result\TextToolResult;
11+
use PHPUnit\Framework\Attributes\Small;
12+
use PHPUnit\Framework\TestCase;
13+
use Symfony\Component\HttpKernel\KernelInterface;
14+
15+
#[Small]
16+
class ProfileGeneratorToolTest extends TestCase
17+
{
18+
private ProfileGeneratorTool $tool;
19+
private string $mockImagePath;
20+
21+
protected function setUp(): void
22+
{
23+
$kernel = $this->createMock(KernelInterface::class);
24+
$kernel->method('getProjectDir')
25+
->willReturn('/tmp/test-project');
26+
27+
$this->mockImagePath = '/tmp/test-project/vendor/klapaudius/symfony-mcp-server/docs/assets/avatar_sample.jpg';
28+
29+
$dir = dirname($this->mockImagePath);
30+
if (!is_dir($dir)) {
31+
mkdir($dir, 0777, true);
32+
}
33+
34+
$imageData = base64_decode('/9j/4AAQSkZJRgABAQEAAAAAAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAr/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=');
35+
file_put_contents($this->mockImagePath, $imageData);
36+
37+
$this->tool = new ProfileGeneratorTool($kernel);
38+
}
39+
40+
protected function tearDown(): void
41+
{
42+
if (file_exists($this->mockImagePath)) {
43+
unlink($this->mockImagePath);
44+
}
45+
46+
$dir = dirname($this->mockImagePath);
47+
while ($dir !== '/tmp' && is_dir($dir)) {
48+
@rmdir($dir);
49+
$dir = dirname($dir);
50+
}
51+
}
52+
53+
public function test_get_name(): void
54+
{
55+
$this->assertEquals('profile-generator', $this->tool->getName());
56+
}
57+
58+
public function test_get_description(): void
59+
{
60+
$expectedDescription = 'Generates a user profile with text description and avatar image';
61+
$this->assertEquals($expectedDescription, $this->tool->getDescription());
62+
}
63+
64+
public function test_get_input_schema(): void
65+
{
66+
$schema = $this->tool->getInputSchema();
67+
68+
$this->assertEquals('object', $schema['type']);
69+
$this->assertArrayHasKey('properties', $schema);
70+
$this->assertArrayHasKey('name', $schema['properties']);
71+
$this->assertArrayHasKey('role', $schema['properties']);
72+
$this->assertEquals(['name', 'role'], $schema['required']);
73+
74+
$this->assertEquals('string', $schema['properties']['name']['type']);
75+
$this->assertEquals('The name of the user', $schema['properties']['name']['description']);
76+
77+
$this->assertEquals('string', $schema['properties']['role']['type']);
78+
$this->assertEquals('The role or profession of the user', $schema['properties']['role']['description']);
79+
}
80+
81+
public function test_get_annotations(): void
82+
{
83+
$annotation = $this->tool->getAnnotations();
84+
85+
$this->assertInstanceOf(ToolAnnotation::class, $annotation);
86+
}
87+
88+
public function test_is_streaming(): void
89+
{
90+
$this->assertTrue($this->tool->isStreaming());
91+
}
92+
93+
public function test_set_progress_notifier(): void
94+
{
95+
$progressNotifier = $this->createMock(ProgressNotifierInterface::class);
96+
97+
$progressNotifier->expects($this->any())
98+
->method('sendProgress');
99+
100+
$this->tool->setProgressNotifier($progressNotifier);
101+
102+
$result = $this->tool->execute(['name' => 'Test', 'role' => 'Test']);
103+
$this->assertInstanceOf(CollectionToolResult::class, $result);
104+
}
105+
}

0 commit comments

Comments
 (0)