Skip to content

Commit 9b534aa

Browse files
committed
minor #12 Add some more tests and docs (Nyholm)
This PR was squashed before being merged into the main branch. Discussion ---------- Add some more tests and docs | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | Docs? | no | Issues | | License | MIT There is one new feature here: A notification can be handled by multiple notification handlers. Commits ------- 2268fd9 Add some more tests and docs
2 parents ec095a3 + 2268fd9 commit 9b534aa

File tree

10 files changed

+234
-6
lines changed

10 files changed

+234
-6
lines changed

src/mcp-sdk/src/Server/JsonRpcHandler.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
namespace Symfony\AI\McpSdk\Server;
1313

1414
use Psr\Log\LoggerInterface;
15+
use Symfony\AI\McpSdk\Exception\ExceptionInterface;
1516
use Symfony\AI\McpSdk\Exception\HandlerNotFoundException;
1617
use Symfony\AI\McpSdk\Exception\NotFoundExceptionInterface;
1718
use Symfony\AI\McpSdk\Message\Error;
1819
use Symfony\AI\McpSdk\Message\Factory;
1920
use Symfony\AI\McpSdk\Message\Notification;
2021
use Symfony\AI\McpSdk\Message\Request;
2122
use Symfony\AI\McpSdk\Message\Response;
22-
use Symfony\Component\String\Exception\ExceptionInterface;
2323

2424
/**
2525
* @final
@@ -51,6 +51,7 @@ public function __construct(
5151
}
5252

5353
/**
54+
* @throws ExceptionInterface
5455
* @throws \JsonException
5556
*/
5657
public function process(string $message): ?string
@@ -88,6 +89,9 @@ public function process(string $message): ?string
8889
}
8990
}
9091

92+
/**
93+
* @throws \JsonException
94+
*/
9195
private function encodeResponse(Response|Error|null $response): ?string
9296
{
9397
if (null === $response) {
@@ -105,15 +109,22 @@ private function encodeResponse(Response|Error|null $response): ?string
105109
return json_encode($response, \JSON_THROW_ON_ERROR);
106110
}
107111

112+
/**
113+
* @throws ExceptionInterface
114+
*/
108115
private function handleNotification(Notification $notification): null
109116
{
117+
$handled = false;
110118
foreach ($this->notificationHandlers as $handler) {
111119
if ($handler->supports($notification)) {
112-
return $handler->handle($notification);
120+
$handler->handle($notification);
121+
$handled = true;
113122
}
114123
}
115124

116-
$this->logger->warning(\sprintf('No handler found for "%s".', $notification->method), ['notification' => $notification]);
125+
if (!$handled) {
126+
$this->logger->warning(\sprintf('No handler found for "%s".', $notification->method), ['notification' => $notification]);
127+
}
117128

118129
return null;
119130
}

src/mcp-sdk/src/Server/NotificationHandler/InitializedHandler.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ protected function supportedNotification(): string
2020
return 'initialized';
2121
}
2222

23-
public function handle(Notification $notification): null
23+
public function handle(Notification $notification): void
2424
{
25-
return null;
2625
}
2726
}

src/mcp-sdk/src/Server/NotificationHandlerInterface.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@
1111

1212
namespace Symfony\AI\McpSdk\Server;
1313

14+
use Symfony\AI\McpSdk\Exception\ExceptionInterface;
1415
use Symfony\AI\McpSdk\Message\Notification;
1516

1617
interface NotificationHandlerInterface
1718
{
1819
public function supports(Notification $message): bool;
1920

20-
public function handle(Notification $notification): null;
21+
/**
22+
* @throws ExceptionInterface
23+
*/
24+
public function handle(Notification $notification): void;
2125
}

src/mcp-sdk/src/Server/RequestHandlerInterface.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\AI\McpSdk\Server;
1313

14+
use Symfony\AI\McpSdk\Exception\ExceptionInterface;
1415
use Symfony\AI\McpSdk\Message\Error;
1516
use Symfony\AI\McpSdk\Message\Request;
1617
use Symfony\AI\McpSdk\Message\Response;
@@ -19,5 +20,8 @@ interface RequestHandlerInterface
1920
{
2021
public function supports(Request $message): bool;
2122

23+
/**
24+
* @throws ExceptionInterface
25+
*/
2226
public function createResponse(Request $message): Response|Error;
2327
}

src/mcp-sdk/tests/Message/ErrorTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests\Message;
1313

14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
1416
use PHPUnit\Framework\TestCase;
1517
use Symfony\AI\McpSdk\Message\Error;
1618

19+
#[Small]
20+
#[CoversClass(Error::class)]
1721
final class ErrorTest extends TestCase
1822
{
1923
public function testWithIntegerId(): void

src/mcp-sdk/tests/Message/FactoryTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests\Message;
1313

14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
1416
use PHPUnit\Framework\TestCase;
1517
use Symfony\AI\McpSdk\Message\Factory;
1618
use Symfony\AI\McpSdk\Message\Notification;
1719
use Symfony\AI\McpSdk\Message\Request;
1820

21+
#[Small]
22+
#[CoversClass(Factory::class)]
1923
final class FactoryTest extends TestCase
2024
{
2125
private Factory $factory;

src/mcp-sdk/tests/Message/ResponseTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests\Message;
1313

14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
1416
use PHPUnit\Framework\TestCase;
1517
use Symfony\AI\McpSdk\Message\Response;
1618

19+
#[Small]
20+
#[CoversClass(Response::class)]
1721
final class ResponseTest extends TestCase
1822
{
1923
public function testWithIntegerId(): void
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 Symfony\AI\McpSdk\Tests\Server;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
16+
use PHPUnit\Framework\Attributes\TestDox;
17+
use PHPUnit\Framework\TestCase;
18+
use Psr\Log\NullLogger;
19+
use Symfony\AI\McpSdk\Message\Factory;
20+
use Symfony\AI\McpSdk\Message\Response;
21+
use Symfony\AI\McpSdk\Server\JsonRpcHandler;
22+
use Symfony\AI\McpSdk\Server\NotificationHandlerInterface;
23+
use Symfony\AI\McpSdk\Server\RequestHandlerInterface;
24+
25+
#[Small]
26+
#[CoversClass(JsonRpcHandler::class)]
27+
class JsonRpcHandlerTest extends TestCase
28+
{
29+
#[TestDox('Make sure a single notification can be handled by multiple handlers.')]
30+
public function testHandleMultipleNotifications(): void
31+
{
32+
$handlerA = $this->getMockBuilder(NotificationHandlerInterface::class)
33+
->disableOriginalConstructor()
34+
->onlyMethods(['supports', 'handle'])
35+
->getMock();
36+
$handlerA->method('supports')->willReturn(true);
37+
$handlerA->expects($this->once())->method('handle');
38+
39+
$handlerB = $this->getMockBuilder(NotificationHandlerInterface::class)
40+
->disableOriginalConstructor()
41+
->onlyMethods(['supports', 'handle'])
42+
->getMock();
43+
$handlerB->method('supports')->willReturn(false);
44+
$handlerB->expects($this->never())->method('handle');
45+
46+
$handlerC = $this->getMockBuilder(NotificationHandlerInterface::class)
47+
->disableOriginalConstructor()
48+
->onlyMethods(['supports', 'handle'])
49+
->getMock();
50+
$handlerC->method('supports')->willReturn(true);
51+
$handlerC->expects($this->once())->method('handle');
52+
53+
$jsonRpc = new JsonRpcHandler(new Factory(), [], [$handlerA, $handlerB, $handlerC], new NullLogger());
54+
$jsonRpc->process(
55+
'{"jsonrpc": "2.0", "id": 1, "method": "notifications/foobar"}'
56+
);
57+
}
58+
59+
#[TestDox('Make sure a single request can NOT be handled by multiple handlers.')]
60+
public function testHandleMultipleRequests(): void
61+
{
62+
$handlerA = $this->getMockBuilder(RequestHandlerInterface::class)
63+
->disableOriginalConstructor()
64+
->onlyMethods(['supports', 'createResponse'])
65+
->getMock();
66+
$handlerA->method('supports')->willReturn(true);
67+
$handlerA->expects($this->once())->method('createResponse')->willReturn(new Response(1));
68+
69+
$handlerB = $this->getMockBuilder(RequestHandlerInterface::class)
70+
->disableOriginalConstructor()
71+
->onlyMethods(['supports', 'createResponse'])
72+
->getMock();
73+
$handlerB->method('supports')->willReturn(false);
74+
$handlerB->expects($this->never())->method('createResponse');
75+
76+
$handlerC = $this->getMockBuilder(RequestHandlerInterface::class)
77+
->disableOriginalConstructor()
78+
->onlyMethods(['supports', 'createResponse'])
79+
->getMock();
80+
$handlerC->method('supports')->willReturn(true);
81+
$handlerC->expects($this->never())->method('createResponse');
82+
83+
$jsonRpc = new JsonRpcHandler(new Factory(), [$handlerA, $handlerB, $handlerC], [], new NullLogger());
84+
$jsonRpc->process(
85+
'{"jsonrpc": "2.0", "id": 1, "method": "request/foobar"}'
86+
);
87+
}
88+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 Symfony\AI\McpSdk\Tests\Server\RequestHandler;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\AI\McpSdk\Capability\Tool\CollectionInterface;
18+
use Symfony\AI\McpSdk\Capability\Tool\MetadataInterface;
19+
use Symfony\AI\McpSdk\Message\Request;
20+
use Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler;
21+
22+
#[Small]
23+
#[CoversClass(ToolListHandler::class)]
24+
class ToolListHandlerTest extends TestCase
25+
{
26+
public function testHandleEmpty(): void
27+
{
28+
$collection = $this->getMockBuilder(CollectionInterface::class)
29+
->disableOriginalConstructor()
30+
->onlyMethods(['getMetadata'])
31+
->getMock();
32+
$collection->expects($this->once())->method('getMetadata')->willReturn([]);
33+
34+
$handler = new ToolListHandler($collection);
35+
$message = new Request(1, 'tools/list', []);
36+
$response = $handler->createResponse($message);
37+
$this->assertEquals(1, $response->id);
38+
$this->assertEquals(['tools' => []], $response->result);
39+
}
40+
41+
public function testHandleReturnAll(): void
42+
{
43+
$item = new class implements MetadataInterface {
44+
public function getName(): string
45+
{
46+
return 'test_tool';
47+
}
48+
49+
public function getDescription(): string
50+
{
51+
return 'A test tool';
52+
}
53+
54+
public function getInputSchema(): array
55+
{
56+
return [
57+
'type' => 'object',
58+
];
59+
}
60+
};
61+
$collection = $this->getMockBuilder(CollectionInterface::class)
62+
->disableOriginalConstructor()
63+
->onlyMethods(['getMetadata'])
64+
->getMock();
65+
$collection->expects($this->once())->method('getMetadata')->willReturn([$item]);
66+
67+
$handler = new ToolListHandler($collection);
68+
$message = new Request(1, 'tools/list', []);
69+
$response = $handler->createResponse($message);
70+
$this->assertCount(1, $response->result['tools']);
71+
$this->assertArrayNotHasKey('nextCursor', $response->result);
72+
}
73+
74+
public function testHandlePagination(): void
75+
{
76+
$item = new class implements MetadataInterface {
77+
public function getName(): string
78+
{
79+
return 'test_tool';
80+
}
81+
82+
public function getDescription(): string
83+
{
84+
return 'A test tool';
85+
}
86+
87+
public function getInputSchema(): array
88+
{
89+
return [
90+
'type' => 'object',
91+
];
92+
}
93+
};
94+
$collection = $this->getMockBuilder(CollectionInterface::class)
95+
->disableOriginalConstructor()
96+
->onlyMethods(['getMetadata'])
97+
->getMock();
98+
$collection->expects($this->once())->method('getMetadata')->willReturn([$item, $item]);
99+
100+
$handler = new ToolListHandler($collection, 2);
101+
$message = new Request(1, 'tools/list', []);
102+
$response = $handler->createResponse($message);
103+
$this->assertCount(2, $response->result['tools']);
104+
$this->assertArrayHasKey('nextCursor', $response->result);
105+
}
106+
}

src/mcp-sdk/tests/ServerTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests;
1313

14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
1416
use PHPUnit\Framework\MockObject\Stub\Exception;
1517
use PHPUnit\Framework\TestCase;
1618
use Psr\Log\NullLogger;
1719
use Symfony\AI\McpSdk\Server;
1820
use Symfony\AI\McpSdk\Server\JsonRpcHandler;
1921
use Symfony\AI\McpSdk\Tests\Fixtures\InMemoryTransport;
2022

23+
#[Small]
24+
#[CoversClass(Server::class)]
2125
class ServerTest extends TestCase
2226
{
2327
public function testJsonExceptions(): void

0 commit comments

Comments
 (0)