Skip to content

Commit 66536ed

Browse files
author
klapaudius
committed
Add unit tests and enhance tool handling logic
Introduced `ToolsCallHandlerTest` for comprehensive testing of tool execution paths and error handling. Updated XML service configuration and schema validation to improve tool argument handling. Adjusted ping interval constraints and corrected transport output formatting in `SseTransport`.
1 parent 2469181 commit 66536ed

File tree

8 files changed

+143
-31
lines changed

8 files changed

+143
-31
lines changed

src/Resources/config/routes.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
->controller([SseController::class, 'handle'])
1818
->methods(['GET', 'POST']);
1919

20-
$routes->add('message_route', "/$defaultPath/message")
20+
$routes->add('message_route', "/$defaultPath/messages")
2121
->controller([MessageController::class, 'handle'])
2222
->methods(['POST']);
2323
};

src/Resources/config/services.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
<argument type="string">%klp_mcp_server.default_path%</argument>
3333
<argument type="service" id="klp_mcp_server.adapter.redis" on-invalid="null" />
3434
<argument type="service" id="logger" on-invalid="null" />
35+
<argument type="binary">%klp_mcp_server.ping.enabled%</argument>
36+
<argument type="string">%klp_mcp_server.ping.interval%</argument>
3537
</service>
3638

3739
<service id="klp_mcp_server.protocol" class="KLP\KlpMcpServer\Protocol\MCPProtocol">

src/Services/ToolService/Examples/HelloWorldTool.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function getInputSchema(): array
2020
{
2121
return [
2222
'type' => 'object',
23-
'properties' => [
23+
'arguments' => [
2424
'name' => [
2525
'type' => 'string',
2626
'description' => 'Developer Name',

src/Services/ToolService/ToolParamsValidator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public static function validate(array $toolSchema, array $arguments): void
6464
}
6565
$valid &= $test;
6666
}
67-
foreach ($toolSchema['required'] as $argument) {
67+
foreach ($toolSchema['required'] ?? [] as $argument) {
6868
$test = ! empty($arguments[$argument]);
6969
if (! $test) {
7070
self::$errors[] = "Missing required argument: $argument";

src/Transports/SseTransport.php

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,36 +45,34 @@ final class SseTransport implements SseTransportInterface
4545
*/
4646
protected array $messageHandlers = [];
4747

48-
/**
49-
* Optional adapter for message persistence and retrieval (e.g., Redis).
50-
* Enables simulation of request/response patterns over SSE.
51-
*/
52-
protected ?SseAdapterInterface $adapter = null;
53-
5448
/**
5549
* Unique identifier for the client connection, generated during initialization.
5650
*/
5751
protected ?string $clientId = null;
5852

59-
60-
protected bool $pingEnabled = false;
61-
6253
/**
6354
* Stores the timestamp of the most recent ping, represented as an integer.
6455
*/
6556
protected int $lastPingTimestamp = 0;
6657

58+
6759
/**
68-
* Defines the interval, in secondes, at which ping messages are sent to maintain the connection.
60+
* Initializes the class with the default path, adapter, logger, and ping settings.
61+
*
62+
* @param string $defaultPath The default path for resources.
63+
* @param SseAdapterInterface|null $adapter Optional adapter for message persistence and retrieval (e.g., Redis).
64+
* Enables simulation of request/response patterns over SSE.
65+
* @param LoggerInterface|null $logger The logger instance (optional).
66+
* @param bool $pingEnabled Flag to enable or disable ping functionality.
67+
* @param int $pingInterval The interval, in secondes, at which ping messages are sent to maintain the connection.
6968
*/
70-
protected int $pingInterval = 60;
71-
7269
public function __construct(
7370
private readonly string $defaultPath,
74-
?SseAdapterInterface $adapter = null,
75-
private readonly ?LoggerInterface $logger = null
71+
private ?SseAdapterInterface $adapter = null,
72+
private readonly ?LoggerInterface $logger = null,
73+
private bool $pingEnabled = false,
74+
private int $pingInterval = 10
7675
) {
77-
$this->adapter = $adapter;
7876
}
7977

8078
/**
@@ -334,7 +332,7 @@ public function pushMessage(string $clientId, array $message): void
334332

335333
private function getEndpoint(string $sessionId): string
336334
{
337-
return sprintf('/%s/message?sessionId=%s',
335+
return sprintf('/%s/messages?sessionId=%s',
338336
trim($this->defaultPath, '/'),
339337
$sessionId,
340338
);
@@ -362,18 +360,18 @@ private function checkPing(): bool
362360
return false;
363361
}
364362
}
365-
return time() - $this->getLastPongResponseTimestamp() < $this->pingInterval + 60;
363+
return time() - $this->getLastPongResponseTimestamp() < $this->pingInterval * 1.8;
366364
}
367365

368366
/**
369367
* Sets the interval for sending ping requests.
370368
*
371369
* @param int $pingInterval The interval in milliseconds at which ping requests should be sent.
372-
* The value must be between 60 and 180 secondes.
370+
* The value must be between 5 and 30 secondes.
373371
*/
374372
protected function setPingInterval(int $pingInterval): void
375373
{
376-
$this->pingInterval = max(60, min($pingInterval, 180));
374+
$this->pingInterval = max(5, min($pingInterval, 30));
377375
}
378376

379377
public function getAdapter(): ?SseAdapterInterface

src/stubs/tool.stub

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class {{ className }} implements ToolInterface
3535
{
3636
return [
3737
'type' => 'object',
38-
'properties' => [
38+
'arguments' => [
3939
'param1' => [
4040
'type' => 'string',
4141
'description' => 'First parameter description',
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace KLP\KlpMcpServer\Tests\Server\Request;
4+
5+
use KLP\KlpMcpServer\Exceptions\JsonRpcErrorException;
6+
use KLP\KlpMcpServer\Exceptions\ToolParamsValidatorException;
7+
use KLP\KlpMcpServer\Server\Request\ToolsCallHandler;
8+
use KLP\KlpMcpServer\Services\ToolService\Examples\HelloWorldTool;
9+
use KLP\KlpMcpServer\Services\ToolService\ToolParamsValidator;
10+
use KLP\KlpMcpServer\Services\ToolService\ToolRepository;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class ToolsCallHandlerTest extends TestCase
14+
{
15+
private ToolRepository $toolRepository;
16+
private ToolsCallHandler $toolsCallHandler;
17+
18+
protected function setUp(): void
19+
{
20+
$this->toolRepository = $this->createMock(ToolRepository::class);
21+
$this->toolsCallHandler = new ToolsCallHandler($this->toolRepository);
22+
}
23+
24+
public function test_execute_throws_exception_when_tool_name_is_missing(): void
25+
{
26+
$this->expectException(JsonRpcErrorException::class);
27+
$this->expectExceptionMessage('Tool name is required');
28+
29+
$this->toolsCallHandler->execute('tools/call', 1, []);
30+
}
31+
32+
public function test_execute_throws_exception_when_tool_not_found(): void
33+
{
34+
$this->toolRepository
35+
->method('getTool')
36+
->with('nonexistent-tool')
37+
->willReturn(null);
38+
39+
$this->expectException(JsonRpcErrorException::class);
40+
$this->expectExceptionMessage("Tool 'nonexistent-tool' not found");
41+
42+
$this->toolsCallHandler->execute('tools/call', 2, ['name' => 'nonexistent-tool']);
43+
}
44+
45+
public function test_execute_throws_exception_for_invalid_arguments(): void
46+
{
47+
$toolMock = $this->createMock(HelloWorldTool::class);
48+
$toolMock->method('getInputSchema')->willReturn(['type' => 'object']);
49+
50+
$this->toolRepository
51+
->method('getTool')
52+
->with('HelloWorldTool')
53+
->willReturn($toolMock);
54+
55+
$this->expectException(ToolParamsValidatorException::class);
56+
57+
ToolParamsValidator::validate(['type' => 'object'], ['invalid' => 'data']);
58+
59+
$this->toolsCallHandler->execute('tools/execute', 3, ['name' => 'HelloWorldTool', 'arguments' => ['invalid' => 'data']]);
60+
}
61+
62+
public function test_execute_returns_content_for_tools_call(): void
63+
{
64+
$this->toolRepository
65+
->method('getTool')
66+
->with('HelloWorldTool')
67+
->willReturn(new HelloWorldTool);
68+
69+
$result = $this->toolsCallHandler->execute('tools/call', 4, ['name' => 'HelloWorldTool', 'arguments' => ['name' => 'Success Message']]);;
70+
71+
$this->assertEquals(
72+
[
73+
'content' => [
74+
['type' => 'text', 'text' => 'Hello, HelloWorld `Success Message` developer.']
75+
]
76+
],
77+
$result
78+
);
79+
}
80+
81+
public function test_execute_returns_result_for_tools_execute(): void
82+
{
83+
$this->toolRepository
84+
->method('getTool')
85+
->with('HelloWorldTool')
86+
->willReturn(new HelloWorldTool);
87+
88+
$result = $this->toolsCallHandler->execute('tools/execute', 5, ['name' => 'HelloWorldTool', 'arguments' => ['name' => 'Success Message']]);;;
89+
90+
$this->assertEquals(
91+
[
92+
'result' => "Hello, HelloWorld `Success Message` developer."
93+
],
94+
$result
95+
);
96+
}
97+
98+
public function test_is_handle_returns_true_for_tools_call(): void
99+
{
100+
$this->assertTrue($this->toolsCallHandler->isHandle('tools/call'));
101+
}
102+
103+
public function test_is_handle_returns_true_for_tools_execute(): void
104+
{
105+
$this->assertTrue($this->toolsCallHandler->isHandle('tools/execute'));
106+
}
107+
108+
public function test_is_handle_returns_false_for_invalid_method(): void
109+
{
110+
$this->assertFalse($this->toolsCallHandler->isHandle('invalid/method'));
111+
}
112+
}

tests/Transports/SseTransportTest.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public function test_initialize_generates_client_id_and_sends_endpoint(): void
7070
// Assert
7171
$expectedClientId = $this->getProtectedProperty($this->instance, 'clientId');
7272
$expectedOutput = 'event: endpoint'.PHP_EOL
73-
.'data: /default-path/message?sessionId='.$expectedClientId.PHP_EOL.PHP_EOL;
73+
.'data: /default-path/messages?sessionId='.$expectedClientId.PHP_EOL.PHP_EOL;
7474

7575
$this->assertNotNull($expectedClientId);
7676
$this->assertEquals($expectedOutput, $output);
@@ -97,7 +97,7 @@ public function test_initialize_does_not_overwrite_existing_client_id(): void
9797
// Assert
9898
$currentClientId = $this->getProtectedProperty($this->instance, 'clientId');
9999
$expectedOutput = 'event: endpoint'.PHP_EOL
100-
.'data: /default-path/message?sessionId='.$existingClientId.PHP_EOL.PHP_EOL;
100+
.'data: /default-path/messages?sessionId='.$existingClientId.PHP_EOL.PHP_EOL;
101101

102102
$this->assertSame($existingClientId, $currentClientId);
103103
$this->assertEquals($expectedOutput, $output);
@@ -631,7 +631,7 @@ private function setProtectedProperty(SseTransport $instance, string $propertyNa
631631
public function test_set_ping_interval_updates_correctly(): void
632632
{
633633
// Arrange
634-
$validPingInterval = 120; // A valid interval in secondes
634+
$validPingInterval = 10; // A valid interval in secondes
635635
$reflection = new \ReflectionClass($this->instance);
636636
$method = $reflection->getMethod('setPingInterval');
637637

@@ -649,7 +649,7 @@ public function test_set_ping_interval_updates_correctly(): void
649649
public function test_set_ping_interval_with_less_than_minimum_value(): void
650650
{
651651
// Arrange
652-
$invalidPingInterval = 10; // Less than the minimum allowed value (60s)
652+
$invalidPingInterval = 1; // Less than the minimum allowed value (5s)
653653
$reflection = new \ReflectionClass($this->instance);
654654
$method = $reflection->getMethod('setPingInterval');
655655

@@ -658,7 +658,7 @@ public function test_set_ping_interval_with_less_than_minimum_value(): void
658658
$updatedPingInterval = $reflection->getProperty('pingInterval')->getValue($this->instance);
659659

660660
// Assert
661-
$this->assertEquals(60, $updatedPingInterval);
661+
$this->assertEquals(5, $updatedPingInterval);
662662
}
663663

664664
/**
@@ -667,7 +667,7 @@ public function test_set_ping_interval_with_less_than_minimum_value(): void
667667
public function test_set_ping_interval_with_more_than_maximum_value(): void
668668
{
669669
// Arrange
670-
$invalidPingInterval = 240; // More than the maximum allowed value (180 s)
670+
$invalidPingInterval = 60; // More than the maximum allowed value (30 s)
671671
$reflection = new \ReflectionClass($this->instance);
672672
$method = $reflection->getMethod('setPingInterval');
673673

@@ -676,7 +676,7 @@ public function test_set_ping_interval_with_more_than_maximum_value(): void
676676
$updatedPingInterval = $reflection->getProperty('pingInterval')->getValue($this->instance);
677677

678678
// Assert
679-
$this->assertEquals(180, $updatedPingInterval);
679+
$this->assertEquals(30, $updatedPingInterval);
680680
}
681681

682682
/**
@@ -779,7 +779,7 @@ public function test_is_connected_triggers_a_ping_message_if_needed(): void
779779
}
780780

781781
// Assert
782-
$this->assertTrue($result, 'Expected isConnected to return false when connection is not active.');
782+
$this->assertFalse($result, 'Expected isConnected to return false when connection is not active.');
783783
$this->assertStringContainsString('"method":"ping"', $output);
784784
}
785785
}

0 commit comments

Comments
 (0)