From 76f064180f17d0064bcf556868a10f17bd374aab Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 00:07:19 +0300 Subject: [PATCH 01/28] Add method to create messages by type --- src/JsonRpc/MessageFactory.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/JsonRpc/MessageFactory.php b/src/JsonRpc/MessageFactory.php index b6e34cc..d6e914c 100644 --- a/src/JsonRpc/MessageFactory.php +++ b/src/JsonRpc/MessageFactory.php @@ -14,10 +14,16 @@ use Mcp\Exception\InvalidArgumentException; use Mcp\Exception\InvalidInputMessageException; use Mcp\Schema\JsonRpc\HasMethodInterface; +use Mcp\Schema\JsonRpc\MessageInterface; use Mcp\Schema\Notification; use Mcp\Schema\Request; /** + * @phpstan-type RequestData array{ + * id: string|int, + * params?: array, + * } + * * @author Christopher Hertel */ final class MessageFactory @@ -103,6 +109,27 @@ public function create(string $input): iterable } } + /** + * Creates a message by its type and parameters. + * + * @template T of value-of + * + * @param class-string $messageType + * @param RequestData $data + * + * @phpstan-return T + */ + public function createByType(string $messageType, array $data): HasMethodInterface + { + if (\in_array($messageType, $this->registeredMessages, true)) { + $data['jsonrpc'] = MessageInterface::JSONRPC_VERSION; + $data['method'] = $messageType::getMethod(); + $messageType::fromArray($data); + } + + throw new InvalidArgumentException(\sprintf('Message type "%s" is not registered.', $messageType)); + } + /** * @return class-string */ From b61828fa7f1cf6970cbeda1815e9af40b4d27e84 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 00:16:31 +0300 Subject: [PATCH 02/28] Parse and queue notifications --- src/JsonRpc/MessageFactory.php | 2 +- src/Server/NotificationPublisher.php | 71 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/Server/NotificationPublisher.php diff --git a/src/JsonRpc/MessageFactory.php b/src/JsonRpc/MessageFactory.php index d6e914c..178a4c3 100644 --- a/src/JsonRpc/MessageFactory.php +++ b/src/JsonRpc/MessageFactory.php @@ -124,7 +124,7 @@ public function createByType(string $messageType, array $data): HasMethodInterfa if (\in_array($messageType, $this->registeredMessages, true)) { $data['jsonrpc'] = MessageInterface::JSONRPC_VERSION; $data['method'] = $messageType::getMethod(); - $messageType::fromArray($data); + return $messageType::fromArray($data); } throw new InvalidArgumentException(\sprintf('Message type "%s" is not registered.', $messageType)); diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php new file mode 100644 index 0000000..c849722 --- /dev/null +++ b/src/Server/NotificationPublisher.php @@ -0,0 +1,71 @@ +, class-string> + */ + private const EVENTS_TO_NOTIFICATIONS = [ + ResourceListChangedEvent::class => ResourceListChangedNotification::class, + PromptListChangedEvent::class => PromptListChangedNotification::class, + ToolListChangedEvent::class => ToolListChangedNotification::class, + ]; + + /** @var list */ + private array $queue = []; + + public function __construct( + private readonly MessageFactory $factory, + ) { + } + + public static function getSubscribedEvents(): array + { + return array_fill_keys(array_keys(self::EVENTS_TO_NOTIFICATIONS), 'onEvent'); + } + + public function onEvent(object $event): void + { + $eventClass = $event::class; + if (!isset(self::EVENTS_TO_NOTIFICATIONS[$eventClass])) { + return; + } + + $notificationType = self::EVENTS_TO_NOTIFICATIONS[$eventClass]; + $notification = $this->factory->createByType($notificationType, []); + + $this->queue[] = $notification; + } + + /** + * Yield and clear queued notifications; Server will encode+send them. + * + * @return iterable + */ + public function flush(): iterable + { + if (!$this->queue) { + return []; + } + + $out = $this->queue; + $this->queue = []; + + return $out; + } +} From e4780c663cefea2e61a237ab14e9fc20670db305 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 00:35:05 +0300 Subject: [PATCH 03/28] Send notifications --- src/Server.php | 6 ++++++ src/Server/NotificationPublisher.php | 12 +++++++----- src/Server/ServerBuilder.php | 2 ++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Server.php b/src/Server.php index fc81382..85c050a 100644 --- a/src/Server.php +++ b/src/Server.php @@ -12,6 +12,7 @@ namespace Mcp; use Mcp\JsonRpc\Handler; +use Mcp\Server\NotificationPublisher; use Mcp\Server\ServerBuilder; use Mcp\Server\TransportInterface; use Psr\Log\LoggerInterface; @@ -24,6 +25,7 @@ final class Server { public function __construct( private readonly Handler $jsonRpcHandler, + private readonly NotificationPublisher $notificationPublisher, private readonly LoggerInterface $logger = new NullLogger(), ) { } @@ -63,6 +65,10 @@ public function connect(TransportInterface $transport): void } } + foreach ($this->notificationPublisher->flush() as $notification) { + $transport->send($notification); + } + usleep(1000); } diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index c849722..4992402 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -8,7 +8,6 @@ use Mcp\Event\ResourceListChangedEvent; use Mcp\Event\ToolListChangedEvent; use Mcp\JsonRpc\MessageFactory; -use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Notification; use Mcp\Schema\Notification\PromptListChangedNotification; use Mcp\Schema\Notification\ResourceListChangedNotification; @@ -26,7 +25,7 @@ final class NotificationPublisher implements EventSubscriberInterface ToolListChangedEvent::class => ToolListChangedNotification::class, ]; - /** @var list */ + /** @var list */ private array $queue = []; public function __construct( @@ -39,6 +38,11 @@ public static function getSubscribedEvents(): array return array_fill_keys(array_keys(self::EVENTS_TO_NOTIFICATIONS), 'onEvent'); } + public static function make(): self + { + return new self(MessageFactory::make()); + } + public function onEvent(object $event): void { $eventClass = $event::class; @@ -53,9 +57,7 @@ public function onEvent(object $event): void } /** - * Yield and clear queued notifications; Server will encode+send them. - * - * @return iterable + * @return iterable */ public function flush(): iterable { diff --git a/src/Server/ServerBuilder.php b/src/Server/ServerBuilder.php index f886756..d814c27 100644 --- a/src/Server/ServerBuilder.php +++ b/src/Server/ServerBuilder.php @@ -23,6 +23,7 @@ use Mcp\Capability\Registry\ReferenceHandler; use Mcp\Exception\ConfigurationException; use Mcp\JsonRpc\Handler; +use Mcp\JsonRpc\MessageFactory; use Mcp\Schema\Annotations; use Mcp\Schema\Implementation; use Mcp\Schema\Prompt; @@ -231,6 +232,7 @@ public function build(): Server return new Server( Handler::make($registry, $this->serverInfo, $logger), + NotificationPublisher::make(), $logger, ); } From f571e71942e37e605fb4e4ef7c1650601110bcfa Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 01:13:11 +0300 Subject: [PATCH 04/28] Clean-up and fix tests --- src/Server/NotificationPublisher.php | 10 ++-------- src/Server/ServerBuilder.php | 1 - tests/ServerTest.php | 6 +++++- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index 4992402..114ddac 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -12,14 +12,13 @@ use Mcp\Schema\Notification\PromptListChangedNotification; use Mcp\Schema\Notification\ResourceListChangedNotification; use Mcp\Schema\Notification\ToolListChangedNotification; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -final class NotificationPublisher implements EventSubscriberInterface +class NotificationPublisher { /** * @var array, class-string> */ - private const EVENTS_TO_NOTIFICATIONS = [ + public const EVENTS_TO_NOTIFICATIONS = [ ResourceListChangedEvent::class => ResourceListChangedNotification::class, PromptListChangedEvent::class => PromptListChangedNotification::class, ToolListChangedEvent::class => ToolListChangedNotification::class, @@ -33,11 +32,6 @@ public function __construct( ) { } - public static function getSubscribedEvents(): array - { - return array_fill_keys(array_keys(self::EVENTS_TO_NOTIFICATIONS), 'onEvent'); - } - public static function make(): self { return new self(MessageFactory::make()); diff --git a/src/Server/ServerBuilder.php b/src/Server/ServerBuilder.php index d814c27..36e15c7 100644 --- a/src/Server/ServerBuilder.php +++ b/src/Server/ServerBuilder.php @@ -23,7 +23,6 @@ use Mcp\Capability\Registry\ReferenceHandler; use Mcp\Exception\ConfigurationException; use Mcp\JsonRpc\Handler; -use Mcp\JsonRpc\MessageFactory; use Mcp\Schema\Annotations; use Mcp\Schema\Implementation; use Mcp\Schema\Prompt; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 2c2129e..fca213c 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -13,6 +13,7 @@ use Mcp\JsonRpc\Handler; use Mcp\Server; +use Mcp\Server\NotificationPublisher; use Mcp\Server\Transport\InMemoryTransport; use PHPUnit\Framework\MockObject\Stub\Exception; use PHPUnit\Framework\TestCase; @@ -34,13 +35,16 @@ public function testJsonExceptions() ->getMock(); $handler->expects($this->exactly(2))->method('process')->willReturnOnConsecutiveCalls(new Exception(new \JsonException('foobar')), ['success']); + $notificationPublisher = $this->createMock(NotificationPublisher::class); + $notificationPublisher->expects($this->once())->method('flush'); + $transport = $this->getMockBuilder(InMemoryTransport::class) ->setConstructorArgs([['foo', 'bar']]) ->onlyMethods(['send']) ->getMock(); $transport->expects($this->once())->method('send')->with('success'); - $server = new Server($handler, $logger); + $server = new Server($handler, $notificationPublisher, $logger); $server->connect($transport); } } From b5622c615d89a633d33f304fbf0f2212934610af Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 01:18:41 +0300 Subject: [PATCH 05/28] Install Symfony dispatcher to handle events for internal usage --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 4d94c1a..1e53610 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "psr/container": "^2.0", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/event-dispatcher": "^7.3", "symfony/finder": "^6.4 || ^7.3", "symfony/uid": "^6.4 || ^7.3" }, From be26b4978614729a67ad1725d5677cf6c326569b Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 01:32:37 +0300 Subject: [PATCH 06/28] Use Symfony dispatcher for internal usage --- src/Event/PromptListChangedEvent.php | 4 +++- src/Event/ResourceListChangedEvent.php | 4 +++- src/Event/ResourceTemplateListChangedEvent.php | 4 +++- src/Event/ToolListChangedEvent.php | 4 +++- src/Server/NotificationPublisher.php | 12 +++++++++--- src/Server/ServerBuilder.php | 15 +++++++++++++-- 6 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/Event/PromptListChangedEvent.php b/src/Event/PromptListChangedEvent.php index 2e86918..f8470a4 100644 --- a/src/Event/PromptListChangedEvent.php +++ b/src/Event/PromptListChangedEvent.php @@ -11,9 +11,11 @@ namespace Mcp\Event; +use Symfony\Contracts\EventDispatcher\Event; + /** * @author Christopher Hertel */ -final class PromptListChangedEvent +final class PromptListChangedEvent extends Event { } diff --git a/src/Event/ResourceListChangedEvent.php b/src/Event/ResourceListChangedEvent.php index 83120d6..5aaad3c 100644 --- a/src/Event/ResourceListChangedEvent.php +++ b/src/Event/ResourceListChangedEvent.php @@ -11,9 +11,11 @@ namespace Mcp\Event; +use Symfony\Contracts\EventDispatcher\Event; + /** * @author Christopher Hertel */ -final class ResourceListChangedEvent +final class ResourceListChangedEvent extends Event { } diff --git a/src/Event/ResourceTemplateListChangedEvent.php b/src/Event/ResourceTemplateListChangedEvent.php index 0c13f65..ce13542 100644 --- a/src/Event/ResourceTemplateListChangedEvent.php +++ b/src/Event/ResourceTemplateListChangedEvent.php @@ -11,9 +11,11 @@ namespace Mcp\Event; +use Symfony\Contracts\EventDispatcher\Event; + /** * @author Christopher Hertel */ -final class ResourceTemplateListChangedEvent +final class ResourceTemplateListChangedEvent extends Event { } diff --git a/src/Event/ToolListChangedEvent.php b/src/Event/ToolListChangedEvent.php index 84d175a..e252fae 100644 --- a/src/Event/ToolListChangedEvent.php +++ b/src/Event/ToolListChangedEvent.php @@ -11,9 +11,11 @@ namespace Mcp\Event; +use Symfony\Contracts\EventDispatcher\Event; + /** * @author Christopher Hertel */ -final class ToolListChangedEvent +final class ToolListChangedEvent extends Event { } diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index 114ddac..6bed107 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -12,11 +12,13 @@ use Mcp\Schema\Notification\PromptListChangedNotification; use Mcp\Schema\Notification\ResourceListChangedNotification; use Mcp\Schema\Notification\ToolListChangedNotification; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Contracts\EventDispatcher\Event; class NotificationPublisher { /** - * @var array, class-string> + * @var array, class-string> */ public const EVENTS_TO_NOTIFICATIONS = [ ResourceListChangedEvent::class => ResourceListChangedNotification::class, @@ -32,9 +34,13 @@ public function __construct( ) { } - public static function make(): self + public static function make(EventDispatcher $eventDispatcher): self { - return new self(MessageFactory::make()); + $instance = new self(MessageFactory::make()); + + $eventDispatcher->addListener(Event::class, [$instance, 'onEvent']); + + return $instance; } public function onEvent(object $event): void diff --git a/src/Server/ServerBuilder.php b/src/Server/ServerBuilder.php index 36e15c7..9b1dfae 100644 --- a/src/Server/ServerBuilder.php +++ b/src/Server/ServerBuilder.php @@ -37,6 +37,8 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Psr\SimpleCache\CacheInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Contracts\EventDispatcher\Event; /** * @author Kyrian Obikwelu @@ -217,10 +219,19 @@ public function withPrompt(callable|array|string $handler, ?string $name = null, */ public function build(): Server { + $internalDispatcher = new EventDispatcher(); + + if ($this->eventDispatcher instanceof EventDispatcherInterface) { + $internalDispatcher->addListener( + Event::class, + fn ($event) => $this->eventDispatcher?->dispatch($event) + ); + } + $logger = $this->logger ?? new NullLogger(); $container = $this->container ?? new Container(); - $registry = new Registry(new ReferenceHandler($container), $this->eventDispatcher, $logger); + $registry = new Registry(new ReferenceHandler($container), $internalDispatcher, $logger); $this->registerManualElements($registry, $logger); @@ -231,7 +242,7 @@ public function build(): Server return new Server( Handler::make($registry, $this->serverInfo, $logger), - NotificationPublisher::make(), + NotificationPublisher::make($internalDispatcher), $logger, ); } From 4af2d4ae178be9fbd7b9f0b9231a207f6fba1bed Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 01:44:09 +0300 Subject: [PATCH 07/28] Test the new notification publisher --- src/Server/NotificationPublisher.php | 2 +- tests/Server/NotificationPublisherTest.php | 36 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/Server/NotificationPublisherTest.php diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index 6bed107..abb46ad 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -43,7 +43,7 @@ public static function make(EventDispatcher $eventDispatcher): self return $instance; } - public function onEvent(object $event): void + public function onEvent(Event $event): void { $eventClass = $event::class; if (!isset(self::EVENTS_TO_NOTIFICATIONS[$eventClass])) { diff --git a/tests/Server/NotificationPublisherTest.php b/tests/Server/NotificationPublisherTest.php new file mode 100644 index 0000000..73216e9 --- /dev/null +++ b/tests/Server/NotificationPublisherTest.php @@ -0,0 +1,36 @@ + $notificationClass) { + /** @var Event $event */ + $event = new $eventClass(); + $notificationPublisher->onEvent($event); + $expectedNotifications[] = $notificationClass; + } + + $flushedNotifications = $notificationPublisher->flush(); + + $this->assertCount(count($expectedNotifications), $flushedNotifications); + + foreach ($flushedNotifications as $index => $notification) { + $this->assertInstanceOf($expectedNotifications[$index], $notification); + } + + $this->assertEmpty($notificationPublisher->flush()); + } +} \ No newline at end of file From d6187dd70662799711531de60fd78a5ac6beb606 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 01:45:04 +0300 Subject: [PATCH 08/28] Fix code style --- tests/Server/NotificationPublisherTest.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/Server/NotificationPublisherTest.php b/tests/Server/NotificationPublisherTest.php index 73216e9..985f489 100644 --- a/tests/Server/NotificationPublisherTest.php +++ b/tests/Server/NotificationPublisherTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the official PHP MCP SDK. + * + * A collaboration between Symfony and the PHP Foundation. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Mcp\Tests\Server; use Mcp\JsonRpc\MessageFactory; @@ -9,6 +18,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Contracts\EventDispatcher\Event; +/** + * @author Aggelos Bellos + */ class NotificationPublisherTest extends TestCase { public function testOnEventCreatesNotification(): void @@ -25,7 +37,7 @@ public function testOnEventCreatesNotification(): void $flushedNotifications = $notificationPublisher->flush(); - $this->assertCount(count($expectedNotifications), $flushedNotifications); + $this->assertCount(\count($expectedNotifications), $flushedNotifications); foreach ($flushedNotifications as $index => $notification) { $this->assertInstanceOf($expectedNotifications[$index], $notification); @@ -33,4 +45,4 @@ public function testOnEventCreatesNotification(): void $this->assertEmpty($notificationPublisher->flush()); } -} \ No newline at end of file +} From 413660b167f6b096d40824582b9e86416d553fb0 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 01:46:52 +0300 Subject: [PATCH 09/28] Add auth annotations --- src/Server/NotificationPublisher.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index abb46ad..36df8e1 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the official PHP MCP SDK. + * + * A collaboration between Symfony and the PHP Foundation. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Mcp\Server; use Mcp\Event\PromptListChangedEvent; @@ -15,6 +24,9 @@ use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Contracts\EventDispatcher\Event; +/** + * @author Aggelos Bellos + */ class NotificationPublisher { /** From 1fa635d989116811fdbf84fcde09361e28f8b9aa Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 01:58:58 +0300 Subject: [PATCH 10/28] Test new message factory method --- tests/JsonRpc/MessageFactoryTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/JsonRpc/MessageFactoryTest.php b/tests/JsonRpc/MessageFactoryTest.php index 9f43aad..287adb2 100644 --- a/tests/JsonRpc/MessageFactoryTest.php +++ b/tests/JsonRpc/MessageFactoryTest.php @@ -11,10 +11,12 @@ namespace Mcp\Tests\JsonRpc; +use Mcp\Exception\InvalidArgumentException; use Mcp\Exception\InvalidInputMessageException; use Mcp\JsonRpc\MessageFactory; use Mcp\Schema\Notification\CancelledNotification; use Mcp\Schema\Notification\InitializedNotification; +use Mcp\Schema\Notification\ResourceListChangedNotification; use Mcp\Schema\Request\GetPromptRequest; use PHPUnit\Framework\TestCase; @@ -81,6 +83,22 @@ public function testBatchMissingMethod() $this->assertInstanceOf(InitializedNotification::class, $result); } + public function testCreateByType(): void + { + $result = $this->factory->createByType(InitializedNotification::class, []); + $this->assertInstanceOf(InitializedNotification::class, $result); + } + + public function testCreateByTypeWithMissingData(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Invalid or missing "requestId" parameter for "notifications/cancelled" notification.' + ); + + $this->factory->createByType(CancelledNotification::class, []); + } + /** * @param iterable $items */ From 138fea6d06e3e8ab2f1c639da1870d0160833382 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 03:37:42 +0300 Subject: [PATCH 11/28] Remove event dispatcher --- composer.json | 2 -- src/Capability/Registry.php | 18 ++++++------- src/Server/NotificationPublisher.php | 11 +++----- src/Server/ServerBuilder.php | 27 +++----------------- tests/Capability/Discovery/DiscoveryTest.php | 3 ++- tests/Server/NotificationPublisherTest.php | 2 +- 6 files changed, 18 insertions(+), 45 deletions(-) diff --git a/composer.json b/composer.json index 1e53610..9de2d0b 100644 --- a/composer.json +++ b/composer.json @@ -23,9 +23,7 @@ "opis/json-schema": "^2.4", "phpdocumentor/reflection-docblock": "^5.6", "psr/container": "^2.0", - "psr/event-dispatcher": "^1.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/event-dispatcher": "^7.3", "symfony/finder": "^6.4 || ^7.3", "symfony/uid": "^6.4 || ^7.3" }, diff --git a/src/Capability/Registry.php b/src/Capability/Registry.php index f0db654..0fc39ab 100644 --- a/src/Capability/Registry.php +++ b/src/Capability/Registry.php @@ -29,7 +29,7 @@ use Mcp\Schema\ResourceTemplate; use Mcp\Schema\ServerCapabilities; use Mcp\Schema\Tool; -use Psr\EventDispatcher\EventDispatcherInterface; +use Mcp\Server\NotificationPublisher; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; @@ -61,8 +61,8 @@ class Registry private array $resourceTemplates = []; public function __construct( + private readonly NotificationPublisher $notificationPublisher, private readonly ReferenceHandler $referenceHandler = new ReferenceHandler(), - private readonly ?EventDispatcherInterface $eventDispatcher = null, private readonly LoggerInterface $logger = new NullLogger(), ) { } @@ -75,12 +75,12 @@ public function getCapabilities(): ServerCapabilities return new ServerCapabilities( tools: true, // [] !== $this->tools, - toolsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface, + toolsListChanged: true, resources: [] !== $this->resources || [] !== $this->resourceTemplates, resourcesSubscribe: false, - resourcesListChanged: $this->eventDispatcher instanceof EventDispatcherInterface, + resourcesListChanged: true, prompts: [] !== $this->prompts, - promptsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface, + promptsListChanged: true, logging: false, // true, completions: true, ); @@ -102,7 +102,7 @@ public function registerTool(Tool $tool, callable|array|string $handler, bool $i $this->tools[$toolName] = new ToolReference($tool, $handler, $isManual); - $this->eventDispatcher?->dispatch(new ToolListChangedEvent()); + $this->notificationPublisher->enqueue(new ToolListChangedEvent()); } /** @@ -121,7 +121,7 @@ public function registerResource(Resource $resource, callable|array|string $hand $this->resources[$uri] = new ResourceReference($resource, $handler, $isManual); - $this->eventDispatcher?->dispatch(new ResourceListChangedEvent()); + $this->notificationPublisher->enqueue(new ResourceListChangedEvent()); } /** @@ -145,7 +145,7 @@ public function registerResourceTemplate( $this->resourceTemplates[$uriTemplate] = new ResourceTemplateReference($template, $handler, $isManual, $completionProviders); - $this->eventDispatcher?->dispatch(new ResourceTemplateListChangedEvent()); + $this->notificationPublisher->enqueue(new ResourceTemplateListChangedEvent()); } /** @@ -169,7 +169,7 @@ public function registerPrompt( $this->prompts[$promptName] = new PromptReference($prompt, $handler, $isManual, $completionProviders); - $this->eventDispatcher?->dispatch(new PromptListChangedEvent()); + $this->notificationPublisher->enqueue(new PromptListChangedEvent()); } /** diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index 36df8e1..e8ad93c 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -21,7 +21,6 @@ use Mcp\Schema\Notification\PromptListChangedNotification; use Mcp\Schema\Notification\ResourceListChangedNotification; use Mcp\Schema\Notification\ToolListChangedNotification; -use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Contracts\EventDispatcher\Event; /** @@ -46,16 +45,12 @@ public function __construct( ) { } - public static function make(EventDispatcher $eventDispatcher): self + public static function make(): self { - $instance = new self(MessageFactory::make()); - - $eventDispatcher->addListener(Event::class, [$instance, 'onEvent']); - - return $instance; + return new self(MessageFactory::make()); } - public function onEvent(Event $event): void + public function enqueue(Event $event): void { $eventClass = $event::class; if (!isset(self::EVENTS_TO_NOTIFICATIONS[$eventClass])) { diff --git a/src/Server/ServerBuilder.php b/src/Server/ServerBuilder.php index 9b1dfae..bf78363 100644 --- a/src/Server/ServerBuilder.php +++ b/src/Server/ServerBuilder.php @@ -33,12 +33,9 @@ use Mcp\Schema\ToolAnnotations; use Mcp\Server; use Psr\Container\ContainerInterface; -use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Psr\SimpleCache\CacheInterface; -use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Contracts\EventDispatcher\Event; /** * @author Kyrian Obikwelu @@ -51,8 +48,6 @@ final class ServerBuilder private ?CacheInterface $cache = null; - private ?EventDispatcherInterface $eventDispatcher = null; - private ?ContainerInterface $container = null; private ?int $paginationLimit = 50; @@ -144,13 +139,6 @@ public function withLogger(LoggerInterface $logger): self return $this; } - public function withEventDispatcher(EventDispatcherInterface $eventDispatcher): self - { - $this->eventDispatcher = $eventDispatcher; - - return $this; - } - /** * Provides a PSR-11 DI container, primarily for resolving user-defined handler classes. * Defaults to a basic internal container. @@ -219,19 +207,10 @@ public function withPrompt(callable|array|string $handler, ?string $name = null, */ public function build(): Server { - $internalDispatcher = new EventDispatcher(); - - if ($this->eventDispatcher instanceof EventDispatcherInterface) { - $internalDispatcher->addListener( - Event::class, - fn ($event) => $this->eventDispatcher?->dispatch($event) - ); - } - $logger = $this->logger ?? new NullLogger(); - + $notificationPublisher = NotificationPublisher::make(); $container = $this->container ?? new Container(); - $registry = new Registry(new ReferenceHandler($container), $internalDispatcher, $logger); + $registry = new Registry($notificationPublisher, new ReferenceHandler($container), $logger); $this->registerManualElements($registry, $logger); @@ -242,7 +221,7 @@ public function build(): Server return new Server( Handler::make($registry, $this->serverInfo, $logger), - NotificationPublisher::make($internalDispatcher), + $notificationPublisher, $logger, ); } diff --git a/tests/Capability/Discovery/DiscoveryTest.php b/tests/Capability/Discovery/DiscoveryTest.php index c6ab3e8..e58d1ac 100644 --- a/tests/Capability/Discovery/DiscoveryTest.php +++ b/tests/Capability/Discovery/DiscoveryTest.php @@ -19,6 +19,7 @@ use Mcp\Capability\Registry\ResourceReference; use Mcp\Capability\Registry\ResourceTemplateReference; use Mcp\Capability\Registry\ToolReference; +use Mcp\Server\NotificationPublisher; use Mcp\Tests\Capability\Attribute\CompletionProviderFixture; use Mcp\Tests\Capability\Discovery\Fixtures\DiscoverableToolHandler; use Mcp\Tests\Capability\Discovery\Fixtures\InvocablePromptFixture; @@ -34,7 +35,7 @@ class DiscoveryTest extends TestCase protected function setUp(): void { - $this->registry = new Registry(); + $this->registry = new Registry(NotificationPublisher::make()); $this->discoverer = new Discoverer($this->registry); } diff --git a/tests/Server/NotificationPublisherTest.php b/tests/Server/NotificationPublisherTest.php index 985f489..291552e 100644 --- a/tests/Server/NotificationPublisherTest.php +++ b/tests/Server/NotificationPublisherTest.php @@ -31,7 +31,7 @@ public function testOnEventCreatesNotification(): void foreach (NotificationPublisher::EVENTS_TO_NOTIFICATIONS as $eventClass => $notificationClass) { /** @var Event $event */ $event = new $eventClass(); - $notificationPublisher->onEvent($event); + $notificationPublisher->enqueue($event); $expectedNotifications[] = $notificationClass; } From abdd1f2f65e6586c4237a187e022ae9344c23bab Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 03:44:41 +0300 Subject: [PATCH 12/28] No need for extra abstractions if we are calling notifications directly --- src/Capability/Registry.php | 16 +++++------ src/Event/PromptListChangedEvent.php | 21 -------------- src/Event/ResourceListChangedEvent.php | 21 -------------- .../ResourceTemplateListChangedEvent.php | 21 -------------- src/Event/ToolListChangedEvent.php | 21 -------------- src/Server/NotificationPublisher.php | 28 ++++--------------- 6 files changed, 13 insertions(+), 115 deletions(-) delete mode 100644 src/Event/PromptListChangedEvent.php delete mode 100644 src/Event/ResourceListChangedEvent.php delete mode 100644 src/Event/ResourceTemplateListChangedEvent.php delete mode 100644 src/Event/ToolListChangedEvent.php diff --git a/src/Capability/Registry.php b/src/Capability/Registry.php index 0fc39ab..25c4ad6 100644 --- a/src/Capability/Registry.php +++ b/src/Capability/Registry.php @@ -17,13 +17,12 @@ use Mcp\Capability\Registry\ResourceReference; use Mcp\Capability\Registry\ResourceTemplateReference; use Mcp\Capability\Registry\ToolReference; -use Mcp\Event\PromptListChangedEvent; -use Mcp\Event\ResourceListChangedEvent; -use Mcp\Event\ResourceTemplateListChangedEvent; -use Mcp\Event\ToolListChangedEvent; use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\Content\PromptMessage; use Mcp\Schema\Content\ResourceContents; +use Mcp\Schema\Notification\PromptListChangedNotification; +use Mcp\Schema\Notification\ResourceListChangedNotification; +use Mcp\Schema\Notification\ToolListChangedNotification; use Mcp\Schema\Prompt; use Mcp\Schema\Resource; use Mcp\Schema\ResourceTemplate; @@ -102,7 +101,7 @@ public function registerTool(Tool $tool, callable|array|string $handler, bool $i $this->tools[$toolName] = new ToolReference($tool, $handler, $isManual); - $this->notificationPublisher->enqueue(new ToolListChangedEvent()); + $this->notificationPublisher->enqueue(ToolListChangedNotification::class); } /** @@ -121,7 +120,7 @@ public function registerResource(Resource $resource, callable|array|string $hand $this->resources[$uri] = new ResourceReference($resource, $handler, $isManual); - $this->notificationPublisher->enqueue(new ResourceListChangedEvent()); + $this->notificationPublisher->enqueue(ResourceListChangedNotification::class); } /** @@ -145,7 +144,8 @@ public function registerResourceTemplate( $this->resourceTemplates[$uriTemplate] = new ResourceTemplateReference($template, $handler, $isManual, $completionProviders); - $this->notificationPublisher->enqueue(new ResourceTemplateListChangedEvent()); + // TODO: Create ResourceTemplateListChangedNotification. + // $this->notificationPublisher->enqueue(ResourceTemplateListChangedNotification::class); } /** @@ -169,7 +169,7 @@ public function registerPrompt( $this->prompts[$promptName] = new PromptReference($prompt, $handler, $isManual, $completionProviders); - $this->notificationPublisher->enqueue(new PromptListChangedEvent()); + $this->notificationPublisher->enqueue(PromptListChangedNotification::class); } /** diff --git a/src/Event/PromptListChangedEvent.php b/src/Event/PromptListChangedEvent.php deleted file mode 100644 index f8470a4..0000000 --- a/src/Event/PromptListChangedEvent.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ -final class PromptListChangedEvent extends Event -{ -} diff --git a/src/Event/ResourceListChangedEvent.php b/src/Event/ResourceListChangedEvent.php deleted file mode 100644 index 5aaad3c..0000000 --- a/src/Event/ResourceListChangedEvent.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ -final class ResourceListChangedEvent extends Event -{ -} diff --git a/src/Event/ResourceTemplateListChangedEvent.php b/src/Event/ResourceTemplateListChangedEvent.php deleted file mode 100644 index ce13542..0000000 --- a/src/Event/ResourceTemplateListChangedEvent.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ -final class ResourceTemplateListChangedEvent extends Event -{ -} diff --git a/src/Event/ToolListChangedEvent.php b/src/Event/ToolListChangedEvent.php deleted file mode 100644 index e252fae..0000000 --- a/src/Event/ToolListChangedEvent.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ -final class ToolListChangedEvent extends Event -{ -} diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index e8ad93c..fc3509d 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -13,30 +13,14 @@ namespace Mcp\Server; -use Mcp\Event\PromptListChangedEvent; -use Mcp\Event\ResourceListChangedEvent; -use Mcp\Event\ToolListChangedEvent; use Mcp\JsonRpc\MessageFactory; use Mcp\Schema\JsonRpc\Notification; -use Mcp\Schema\Notification\PromptListChangedNotification; -use Mcp\Schema\Notification\ResourceListChangedNotification; -use Mcp\Schema\Notification\ToolListChangedNotification; -use Symfony\Contracts\EventDispatcher\Event; /** * @author Aggelos Bellos */ class NotificationPublisher { - /** - * @var array, class-string> - */ - public const EVENTS_TO_NOTIFICATIONS = [ - ResourceListChangedEvent::class => ResourceListChangedNotification::class, - PromptListChangedEvent::class => PromptListChangedNotification::class, - ToolListChangedEvent::class => ToolListChangedNotification::class, - ]; - /** @var list */ private array $queue = []; @@ -50,14 +34,12 @@ public static function make(): self return new self(MessageFactory::make()); } - public function enqueue(Event $event): void + /** + * @param class-string $notificationType + * @return void + */ + public function enqueue(string $notificationType): void { - $eventClass = $event::class; - if (!isset(self::EVENTS_TO_NOTIFICATIONS[$eventClass])) { - return; - } - - $notificationType = self::EVENTS_TO_NOTIFICATIONS[$eventClass]; $notification = $this->factory->createByType($notificationType, []); $this->queue[] = $notification; From fde283cf8ca4678b4e190f9215d7e4bb8962cd16 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 12:15:10 +0300 Subject: [PATCH 13/28] Server accepts different argument now --- examples/09-standalone-cli/index.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/09-standalone-cli/index.php b/examples/09-standalone-cli/index.php index f7a6742..298ba64 100644 --- a/examples/09-standalone-cli/index.php +++ b/examples/09-standalone-cli/index.php @@ -27,8 +27,10 @@ $logger ); +$notificationPublisher = Mcp\Server\NotificationPublisher::make(); + // Set up the server -$sever = new Mcp\Server($jsonRpcHandler, $logger); +$sever = new Mcp\Server($jsonRpcHandler, $notificationPublisher, $logger); // Create the transport layer using Stdio $transport = new Mcp\Server\Transport\StdioTransport(logger: $logger); From 6beda2d104640ab460f960c4faa1965c1bdce2fd Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 12:15:38 +0300 Subject: [PATCH 14/28] Fix return type --- src/JsonRpc/MessageFactory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JsonRpc/MessageFactory.php b/src/JsonRpc/MessageFactory.php index 178a4c3..24fa977 100644 --- a/src/JsonRpc/MessageFactory.php +++ b/src/JsonRpc/MessageFactory.php @@ -112,12 +112,12 @@ public function create(string $input): iterable /** * Creates a message by its type and parameters. * - * @template T of value-of + * @template T of HasMethodInterface * * @param class-string $messageType * @param RequestData $data * - * @phpstan-return T + * @return T */ public function createByType(string $messageType, array $data): HasMethodInterface { From de10bcdb928449801e41141cee39f815c9ab557d Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 12:15:49 +0300 Subject: [PATCH 15/28] No need for an id --- src/JsonRpc/MessageFactory.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/JsonRpc/MessageFactory.php b/src/JsonRpc/MessageFactory.php index 24fa977..d81db97 100644 --- a/src/JsonRpc/MessageFactory.php +++ b/src/JsonRpc/MessageFactory.php @@ -20,7 +20,6 @@ /** * @phpstan-type RequestData array{ - * id: string|int, * params?: array, * } * From 8c8148710deada077837a0c6f5261cb3263098d7 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 12:16:33 +0300 Subject: [PATCH 16/28] Clean-up phpdoc --- src/Server/NotificationPublisher.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index fc3509d..18642cf 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -36,7 +36,6 @@ public static function make(): self /** * @param class-string $notificationType - * @return void */ public function enqueue(string $notificationType): void { From 8b201e1d46a731fe47fbb4bc1b187ac4337b70b6 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 12:29:33 +0300 Subject: [PATCH 17/28] Update tests --- tests/JsonRpc/MessageFactoryTest.php | 1 - tests/Server/NotificationPublisherTest.php | 19 +++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/JsonRpc/MessageFactoryTest.php b/tests/JsonRpc/MessageFactoryTest.php index 287adb2..72255fd 100644 --- a/tests/JsonRpc/MessageFactoryTest.php +++ b/tests/JsonRpc/MessageFactoryTest.php @@ -16,7 +16,6 @@ use Mcp\JsonRpc\MessageFactory; use Mcp\Schema\Notification\CancelledNotification; use Mcp\Schema\Notification\InitializedNotification; -use Mcp\Schema\Notification\ResourceListChangedNotification; use Mcp\Schema\Request\GetPromptRequest; use PHPUnit\Framework\TestCase; diff --git a/tests/Server/NotificationPublisherTest.php b/tests/Server/NotificationPublisherTest.php index 291552e..3cff7e8 100644 --- a/tests/Server/NotificationPublisherTest.php +++ b/tests/Server/NotificationPublisherTest.php @@ -14,25 +14,28 @@ namespace Mcp\Tests\Server; use Mcp\JsonRpc\MessageFactory; +use Mcp\Schema\Notification\PromptListChangedNotification; +use Mcp\Schema\Notification\ResourceListChangedNotification; +use Mcp\Schema\Notification\ToolListChangedNotification; use Mcp\Server\NotificationPublisher; use PHPUnit\Framework\TestCase; -use Symfony\Contracts\EventDispatcher\Event; /** * @author Aggelos Bellos */ class NotificationPublisherTest extends TestCase { - public function testOnEventCreatesNotification(): void + public function testEnqueue(): void { - $expectedNotifications = []; + $expectedNotifications = [ + ToolListChangedNotification::class, + ResourceListChangedNotification::class, + PromptListChangedNotification::class, + ]; $notificationPublisher = new NotificationPublisher(MessageFactory::make()); - foreach (NotificationPublisher::EVENTS_TO_NOTIFICATIONS as $eventClass => $notificationClass) { - /** @var Event $event */ - $event = new $eventClass(); - $notificationPublisher->enqueue($event); - $expectedNotifications[] = $notificationClass; + foreach ($expectedNotifications as $notificationType) { + $notificationPublisher->enqueue($notificationType); } $flushedNotifications = $notificationPublisher->flush(); From 4900058c004418e247f35a3971433b5b80b4022d Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 12:29:54 +0300 Subject: [PATCH 18/28] Fix sending notifications --- src/Server.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index 85c050a..7e6b67d 100644 --- a/src/Server.php +++ b/src/Server.php @@ -66,7 +66,15 @@ public function connect(TransportInterface $transport): void } foreach ($this->notificationPublisher->flush() as $notification) { - $transport->send($notification); + try { + $transport->send(json_encode($notification, \JSON_THROW_ON_ERROR)); + } catch (\JsonException $e) { + $this->logger->error('Failed to encode notification to JSON.', [ + 'notification' => $notification::class, + 'exception' => $e, + ]); + continue; + } } usleep(1000); From 7a31c24f8f77af8dacdb3774e1d5347c5b84dc11 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 12:38:57 +0300 Subject: [PATCH 19/28] Use generator to return notifications --- src/Server/NotificationPublisher.php | 8 ++------ tests/Server/NotificationPublisherTest.php | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index 18642cf..70a1788 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -45,17 +45,13 @@ public function enqueue(string $notificationType): void } /** - * @return iterable + * @return \Generator */ public function flush(): iterable { - if (!$this->queue) { - return []; - } - $out = $this->queue; $this->queue = []; - return $out; + yield from $out; } } diff --git a/tests/Server/NotificationPublisherTest.php b/tests/Server/NotificationPublisherTest.php index 3cff7e8..f8b8a5e 100644 --- a/tests/Server/NotificationPublisherTest.php +++ b/tests/Server/NotificationPublisherTest.php @@ -38,7 +38,7 @@ public function testEnqueue(): void $notificationPublisher->enqueue($notificationType); } - $flushedNotifications = $notificationPublisher->flush(); + $flushedNotifications = iterator_to_array($notificationPublisher->flush()); $this->assertCount(\count($expectedNotifications), $flushedNotifications); @@ -46,6 +46,6 @@ public function testEnqueue(): void $this->assertInstanceOf($expectedNotifications[$index], $notification); } - $this->assertEmpty($notificationPublisher->flush()); + $this->assertEmpty(iterator_to_array($notificationPublisher->flush())); } } From efd3c0a4a8a75343a159c47bb56c9f8f33b1d7b7 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Sun, 7 Sep 2025 13:04:37 +0300 Subject: [PATCH 20/28] Test Registry --- src/JsonRpc/MessageFactory.php | 1 + tests/Capability/RegistryTest.php | 96 +++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 tests/Capability/RegistryTest.php diff --git a/src/JsonRpc/MessageFactory.php b/src/JsonRpc/MessageFactory.php index d81db97..72a3dbd 100644 --- a/src/JsonRpc/MessageFactory.php +++ b/src/JsonRpc/MessageFactory.php @@ -123,6 +123,7 @@ public function createByType(string $messageType, array $data): HasMethodInterfa if (\in_array($messageType, $this->registeredMessages, true)) { $data['jsonrpc'] = MessageInterface::JSONRPC_VERSION; $data['method'] = $messageType::getMethod(); + return $messageType::fromArray($data); } diff --git a/tests/Capability/RegistryTest.php b/tests/Capability/RegistryTest.php new file mode 100644 index 0000000..52e83a1 --- /dev/null +++ b/tests/Capability/RegistryTest.php @@ -0,0 +1,96 @@ + 'object', + 'properties' => [ + 'param1' => ['type' => 'string'], + ], + 'required' => [ + 'param1', + ], + ], + null, + null + ); + + $expected = [$tool->name => $tool]; + + $notificationPublisher = $this->createMock(NotificationPublisher::class); + $notificationPublisher->expects($this->once()) + ->method('enqueue') + ->with(ToolListChangedNotification::class); + + $registry = new Registry($notificationPublisher); + $registry->registerTool($tool, fn () => null); + + $this->assertSame($expected, $registry->getTools()); + } + + public function testResourceRegistration(): void + { + $resource = new Resource( + 'config://the-best-resource-uri-ever', + 'the-best-resource-name-ever', + ); + + $expected = [$resource->uri => $resource]; + + $notificationPublisher = $this->createMock(NotificationPublisher::class); + $notificationPublisher->expects($this->once()) + ->method('enqueue') + ->with(ResourceListChangedNotification::class); + + $registry = new Registry($notificationPublisher); + $registry->registerResource($resource, fn () => null); + + $this->assertSame($expected, $registry->getResources()); + } + + public function testPromptRegistration(): void + { + $prompt = new Prompt( + 'the-best-prompt-ever', + ); + + $expected = [$prompt->name => $prompt]; + + $notificationPublisher = $this->createMock(NotificationPublisher::class); + $notificationPublisher->expects($this->once()) + ->method('enqueue') + ->with(PromptListChangedNotification::class); + + $registry = new Registry($notificationPublisher); + $registry->registerPrompt($prompt, fn () => null); + + $this->assertSame($expected, $registry->getPrompts()); + } +} From 8c55c25446622508686a49001225ad7b6939e982 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Wed, 10 Sep 2025 01:08:35 +0300 Subject: [PATCH 21/28] Let the caller handle the construction of the notification --- examples/09-standalone-cli/index.php | 2 +- src/Capability/Registry.php | 6 +++--- src/Server/NotificationPublisher.php | 18 +----------------- src/Server/ServerBuilder.php | 2 +- tests/Capability/Discovery/DiscoveryTest.php | 2 +- tests/Capability/RegistryTest.php | 6 +++--- tests/Server/NotificationPublisherTest.php | 10 +++++----- 7 files changed, 15 insertions(+), 31 deletions(-) diff --git a/examples/09-standalone-cli/index.php b/examples/09-standalone-cli/index.php index 298ba64..a2b4dd4 100644 --- a/examples/09-standalone-cli/index.php +++ b/examples/09-standalone-cli/index.php @@ -27,7 +27,7 @@ $logger ); -$notificationPublisher = Mcp\Server\NotificationPublisher::make(); +$notificationPublisher = new Mcp\Server\NotificationPublisher(); // Set up the server $sever = new Mcp\Server($jsonRpcHandler, $notificationPublisher, $logger); diff --git a/src/Capability/Registry.php b/src/Capability/Registry.php index 25c4ad6..0aeee27 100644 --- a/src/Capability/Registry.php +++ b/src/Capability/Registry.php @@ -101,7 +101,7 @@ public function registerTool(Tool $tool, callable|array|string $handler, bool $i $this->tools[$toolName] = new ToolReference($tool, $handler, $isManual); - $this->notificationPublisher->enqueue(ToolListChangedNotification::class); + $this->notificationPublisher->enqueue(new ToolListChangedNotification()); } /** @@ -120,7 +120,7 @@ public function registerResource(Resource $resource, callable|array|string $hand $this->resources[$uri] = new ResourceReference($resource, $handler, $isManual); - $this->notificationPublisher->enqueue(ResourceListChangedNotification::class); + $this->notificationPublisher->enqueue(new ResourceListChangedNotification()); } /** @@ -169,7 +169,7 @@ public function registerPrompt( $this->prompts[$promptName] = new PromptReference($prompt, $handler, $isManual, $completionProviders); - $this->notificationPublisher->enqueue(PromptListChangedNotification::class); + $this->notificationPublisher->enqueue(new PromptListChangedNotification()); } /** diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index 70a1788..ccad182 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -13,7 +13,6 @@ namespace Mcp\Server; -use Mcp\JsonRpc\MessageFactory; use Mcp\Schema\JsonRpc\Notification; /** @@ -24,23 +23,8 @@ class NotificationPublisher /** @var list */ private array $queue = []; - public function __construct( - private readonly MessageFactory $factory, - ) { - } - - public static function make(): self - { - return new self(MessageFactory::make()); - } - - /** - * @param class-string $notificationType - */ - public function enqueue(string $notificationType): void + public function enqueue(Notification $notification): void { - $notification = $this->factory->createByType($notificationType, []); - $this->queue[] = $notification; } diff --git a/src/Server/ServerBuilder.php b/src/Server/ServerBuilder.php index bf78363..423a13b 100644 --- a/src/Server/ServerBuilder.php +++ b/src/Server/ServerBuilder.php @@ -208,7 +208,7 @@ public function withPrompt(callable|array|string $handler, ?string $name = null, public function build(): Server { $logger = $this->logger ?? new NullLogger(); - $notificationPublisher = NotificationPublisher::make(); + $notificationPublisher = new NotificationPublisher(); $container = $this->container ?? new Container(); $registry = new Registry($notificationPublisher, new ReferenceHandler($container), $logger); diff --git a/tests/Capability/Discovery/DiscoveryTest.php b/tests/Capability/Discovery/DiscoveryTest.php index e58d1ac..2d330dc 100644 --- a/tests/Capability/Discovery/DiscoveryTest.php +++ b/tests/Capability/Discovery/DiscoveryTest.php @@ -35,7 +35,7 @@ class DiscoveryTest extends TestCase protected function setUp(): void { - $this->registry = new Registry(NotificationPublisher::make()); + $this->registry = new Registry(new NotificationPublisher()); $this->discoverer = new Discoverer($this->registry); } diff --git a/tests/Capability/RegistryTest.php b/tests/Capability/RegistryTest.php index 52e83a1..3effeb8 100644 --- a/tests/Capability/RegistryTest.php +++ b/tests/Capability/RegistryTest.php @@ -47,7 +47,7 @@ public function testToolRegistration(): void $notificationPublisher = $this->createMock(NotificationPublisher::class); $notificationPublisher->expects($this->once()) ->method('enqueue') - ->with(ToolListChangedNotification::class); + ->with(new ToolListChangedNotification()); $registry = new Registry($notificationPublisher); $registry->registerTool($tool, fn () => null); @@ -67,7 +67,7 @@ public function testResourceRegistration(): void $notificationPublisher = $this->createMock(NotificationPublisher::class); $notificationPublisher->expects($this->once()) ->method('enqueue') - ->with(ResourceListChangedNotification::class); + ->with(new ResourceListChangedNotification()); $registry = new Registry($notificationPublisher); $registry->registerResource($resource, fn () => null); @@ -86,7 +86,7 @@ public function testPromptRegistration(): void $notificationPublisher = $this->createMock(NotificationPublisher::class); $notificationPublisher->expects($this->once()) ->method('enqueue') - ->with(PromptListChangedNotification::class); + ->with(new PromptListChangedNotification()); $registry = new Registry($notificationPublisher); $registry->registerPrompt($prompt, fn () => null); diff --git a/tests/Server/NotificationPublisherTest.php b/tests/Server/NotificationPublisherTest.php index f8b8a5e..e096b64 100644 --- a/tests/Server/NotificationPublisherTest.php +++ b/tests/Server/NotificationPublisherTest.php @@ -28,11 +28,11 @@ class NotificationPublisherTest extends TestCase public function testEnqueue(): void { $expectedNotifications = [ - ToolListChangedNotification::class, - ResourceListChangedNotification::class, - PromptListChangedNotification::class, + new ToolListChangedNotification(), + new ResourceListChangedNotification(), + new PromptListChangedNotification(), ]; - $notificationPublisher = new NotificationPublisher(MessageFactory::make()); + $notificationPublisher = new NotificationPublisher(); foreach ($expectedNotifications as $notificationType) { $notificationPublisher->enqueue($notificationType); @@ -43,7 +43,7 @@ public function testEnqueue(): void $this->assertCount(\count($expectedNotifications), $flushedNotifications); foreach ($flushedNotifications as $index => $notification) { - $this->assertInstanceOf($expectedNotifications[$index], $notification); + $this->assertSame($expectedNotifications[$index], $notification); } $this->assertEmpty(iterator_to_array($notificationPublisher->flush())); From f39c6140a12993d2d0a0eb5b8508bd5ce6b1f6c9 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Wed, 10 Sep 2025 01:30:34 +0300 Subject: [PATCH 22/28] Remove unused method --- src/JsonRpc/MessageFactory.php | 24 +----------------------- tests/JsonRpc/MessageFactoryTest.php | 16 ---------------- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/src/JsonRpc/MessageFactory.php b/src/JsonRpc/MessageFactory.php index 72a3dbd..a6c5e3c 100644 --- a/src/JsonRpc/MessageFactory.php +++ b/src/JsonRpc/MessageFactory.php @@ -108,32 +108,10 @@ public function create(string $input): iterable } } - /** - * Creates a message by its type and parameters. - * - * @template T of HasMethodInterface - * - * @param class-string $messageType - * @param RequestData $data - * - * @return T - */ - public function createByType(string $messageType, array $data): HasMethodInterface - { - if (\in_array($messageType, $this->registeredMessages, true)) { - $data['jsonrpc'] = MessageInterface::JSONRPC_VERSION; - $data['method'] = $messageType::getMethod(); - - return $messageType::fromArray($data); - } - - throw new InvalidArgumentException(\sprintf('Message type "%s" is not registered.', $messageType)); - } - /** * @return class-string */ - private function getType(string $method): string + private function getType(string $method/**/): string { foreach (self::REGISTERED_MESSAGES as $type) { if ($type::getMethod() === $method) { diff --git a/tests/JsonRpc/MessageFactoryTest.php b/tests/JsonRpc/MessageFactoryTest.php index 72255fd..9945f7f 100644 --- a/tests/JsonRpc/MessageFactoryTest.php +++ b/tests/JsonRpc/MessageFactoryTest.php @@ -82,22 +82,6 @@ public function testBatchMissingMethod() $this->assertInstanceOf(InitializedNotification::class, $result); } - public function testCreateByType(): void - { - $result = $this->factory->createByType(InitializedNotification::class, []); - $this->assertInstanceOf(InitializedNotification::class, $result); - } - - public function testCreateByTypeWithMissingData(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage( - 'Invalid or missing "requestId" parameter for "notifications/cancelled" notification.' - ); - - $this->factory->createByType(CancelledNotification::class, []); - } - /** * @param iterable $items */ From 759a379ec02f9ee475bcc23ae6d939552399137c Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Wed, 10 Sep 2025 01:55:39 +0300 Subject: [PATCH 23/28] Fix code style --- src/JsonRpc/MessageFactory.php | 3 +-- tests/JsonRpc/MessageFactoryTest.php | 1 - tests/Server/NotificationPublisherTest.php | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/JsonRpc/MessageFactory.php b/src/JsonRpc/MessageFactory.php index a6c5e3c..4f351d4 100644 --- a/src/JsonRpc/MessageFactory.php +++ b/src/JsonRpc/MessageFactory.php @@ -14,7 +14,6 @@ use Mcp\Exception\InvalidArgumentException; use Mcp\Exception\InvalidInputMessageException; use Mcp\Schema\JsonRpc\HasMethodInterface; -use Mcp\Schema\JsonRpc\MessageInterface; use Mcp\Schema\Notification; use Mcp\Schema\Request; @@ -111,7 +110,7 @@ public function create(string $input): iterable /** * @return class-string */ - private function getType(string $method/**/): string + private function getType(string $method): string { foreach (self::REGISTERED_MESSAGES as $type) { if ($type::getMethod() === $method) { diff --git a/tests/JsonRpc/MessageFactoryTest.php b/tests/JsonRpc/MessageFactoryTest.php index 9945f7f..9f43aad 100644 --- a/tests/JsonRpc/MessageFactoryTest.php +++ b/tests/JsonRpc/MessageFactoryTest.php @@ -11,7 +11,6 @@ namespace Mcp\Tests\JsonRpc; -use Mcp\Exception\InvalidArgumentException; use Mcp\Exception\InvalidInputMessageException; use Mcp\JsonRpc\MessageFactory; use Mcp\Schema\Notification\CancelledNotification; diff --git a/tests/Server/NotificationPublisherTest.php b/tests/Server/NotificationPublisherTest.php index e096b64..bedb1fd 100644 --- a/tests/Server/NotificationPublisherTest.php +++ b/tests/Server/NotificationPublisherTest.php @@ -13,7 +13,6 @@ namespace Mcp\Tests\Server; -use Mcp\JsonRpc\MessageFactory; use Mcp\Schema\Notification\PromptListChangedNotification; use Mcp\Schema\Notification\ResourceListChangedNotification; use Mcp\Schema\Notification\ToolListChangedNotification; From f226b2b82186dc57e635596e7868fe0099079a8a Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Wed, 10 Sep 2025 02:01:27 +0300 Subject: [PATCH 24/28] Simplify logic --- src/Server/NotificationPublisher.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Server/NotificationPublisher.php b/src/Server/NotificationPublisher.php index ccad182..b81afd3 100644 --- a/src/Server/NotificationPublisher.php +++ b/src/Server/NotificationPublisher.php @@ -33,9 +33,8 @@ public function enqueue(Notification $notification): void */ public function flush(): iterable { - $out = $this->queue; - $this->queue = []; + yield from $this->queue; - yield from $out; + $this->queue = []; } } From 4c2415b7d4a916f41a5e8245db667a78fe1ea649 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Wed, 10 Sep 2025 02:05:41 +0300 Subject: [PATCH 25/28] Make NotificationPublisher optional --- src/Capability/Registry.php | 2 +- tests/Capability/Discovery/DiscoveryTest.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Capability/Registry.php b/src/Capability/Registry.php index 0aeee27..1596d69 100644 --- a/src/Capability/Registry.php +++ b/src/Capability/Registry.php @@ -60,7 +60,7 @@ class Registry private array $resourceTemplates = []; public function __construct( - private readonly NotificationPublisher $notificationPublisher, + private readonly NotificationPublisher $notificationPublisher = new NotificationPublisher(), private readonly ReferenceHandler $referenceHandler = new ReferenceHandler(), private readonly LoggerInterface $logger = new NullLogger(), ) { diff --git a/tests/Capability/Discovery/DiscoveryTest.php b/tests/Capability/Discovery/DiscoveryTest.php index 2d330dc..c6ab3e8 100644 --- a/tests/Capability/Discovery/DiscoveryTest.php +++ b/tests/Capability/Discovery/DiscoveryTest.php @@ -19,7 +19,6 @@ use Mcp\Capability\Registry\ResourceReference; use Mcp\Capability\Registry\ResourceTemplateReference; use Mcp\Capability\Registry\ToolReference; -use Mcp\Server\NotificationPublisher; use Mcp\Tests\Capability\Attribute\CompletionProviderFixture; use Mcp\Tests\Capability\Discovery\Fixtures\DiscoverableToolHandler; use Mcp\Tests\Capability\Discovery\Fixtures\InvocablePromptFixture; @@ -35,7 +34,7 @@ class DiscoveryTest extends TestCase protected function setUp(): void { - $this->registry = new Registry(new NotificationPublisher()); + $this->registry = new Registry(); $this->discoverer = new Discoverer($this->registry); } From 4a29253ddd7ab1f5c7d88ae545c1268417384c53 Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Mon, 15 Sep 2025 23:53:19 +0300 Subject: [PATCH 26/28] Update tests based on new constructor --- tests/Capability/Registry/RegistryTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Capability/Registry/RegistryTest.php b/tests/Capability/Registry/RegistryTest.php index 14548b6..b3c5e9d 100644 --- a/tests/Capability/Registry/RegistryTest.php +++ b/tests/Capability/Registry/RegistryTest.php @@ -18,6 +18,7 @@ use Mcp\Schema\ResourceTemplate; use Mcp\Schema\ServerCapabilities; use Mcp\Schema\Tool; +use Mcp\Server\NotificationPublisher; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -30,7 +31,7 @@ class RegistryTest extends TestCase protected function setUp(): void { $this->logger = $this->createMock(LoggerInterface::class); - $this->registry = new Registry(null, $this->logger); + $this->registry = new Registry(new NotificationPublisher(), $this->logger); } public function testConstructorWithDefaults(): void @@ -39,9 +40,6 @@ public function testConstructorWithDefaults(): void $capabilities = $registry->getCapabilities(); $this->assertInstanceOf(ServerCapabilities::class, $capabilities); - $this->assertFalse($capabilities->toolsListChanged); - $this->assertFalse($capabilities->resourcesListChanged); - $this->assertFalse($capabilities->promptsListChanged); } public function testGetCapabilitiesWhenEmpty(): void From 244a81e20661b8d66f0aa758fa44c45af79df25a Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Mon, 15 Sep 2025 23:57:06 +0300 Subject: [PATCH 27/28] Unify tests --- tests/Capability/Registry/RegistryTest.php | 54 ++++++++++++ tests/Capability/RegistryTest.php | 96 ---------------------- 2 files changed, 54 insertions(+), 96 deletions(-) delete mode 100644 tests/Capability/RegistryTest.php diff --git a/tests/Capability/Registry/RegistryTest.php b/tests/Capability/Registry/RegistryTest.php index b3c5e9d..a2cb329 100644 --- a/tests/Capability/Registry/RegistryTest.php +++ b/tests/Capability/Registry/RegistryTest.php @@ -13,6 +13,9 @@ use Mcp\Capability\Prompt\Completion\EnumCompletionProvider; use Mcp\Capability\Registry; +use Mcp\Schema\Notification\PromptListChangedNotification; +use Mcp\Schema\Notification\ResourceListChangedNotification; +use Mcp\Schema\Notification\ToolListChangedNotification; use Mcp\Schema\Prompt; use Mcp\Schema\Resource; use Mcp\Schema\ResourceTemplate; @@ -305,6 +308,57 @@ public function testMultipleRegistrationsOfSameElementWithSameType(): void $this->assertEquals('second', ($toolRef->handler)()); } + public function testToolRegistrationTriggersNotification(): void + { + $tool = $this->createValidTool('the-best-tool-name-ever'); + + $expected = [$tool->name => $tool]; + + $notificationPublisher = $this->createMock(NotificationPublisher::class); + $notificationPublisher->expects($this->once()) + ->method('enqueue') + ->with(new ToolListChangedNotification()); + + $registry = new Registry($notificationPublisher); + $registry->registerTool($tool, fn () => null); + + $this->assertSame($expected, $registry->getTools()); + } + + public function testResourceRegistrationTriggersNotification(): void + { + $resource = $this->createValidResource('config://the-best-resource-uri-ever'); + + $expected = [$resource->uri => $resource]; + + $notificationPublisher = $this->createMock(NotificationPublisher::class); + $notificationPublisher->expects($this->once()) + ->method('enqueue') + ->with(new ResourceListChangedNotification()); + + $registry = new Registry($notificationPublisher); + $registry->registerResource($resource, fn () => null); + + $this->assertSame($expected, $registry->getResources()); + } + + public function testPromptRegistrationTriggersNotification(): void + { + $prompt = $this->createValidPrompt('the-best-prompt-ever'); + + $expected = [$prompt->name => $prompt]; + + $notificationPublisher = $this->createMock(NotificationPublisher::class); + $notificationPublisher->expects($this->once()) + ->method('enqueue') + ->with(new PromptListChangedNotification()); + + $registry = new Registry($notificationPublisher); + $registry->registerPrompt($prompt, fn () => null); + + $this->assertSame($expected, $registry->getPrompts()); + } + private function createValidTool(string $name): Tool { return new Tool( diff --git a/tests/Capability/RegistryTest.php b/tests/Capability/RegistryTest.php deleted file mode 100644 index 3effeb8..0000000 --- a/tests/Capability/RegistryTest.php +++ /dev/null @@ -1,96 +0,0 @@ - 'object', - 'properties' => [ - 'param1' => ['type' => 'string'], - ], - 'required' => [ - 'param1', - ], - ], - null, - null - ); - - $expected = [$tool->name => $tool]; - - $notificationPublisher = $this->createMock(NotificationPublisher::class); - $notificationPublisher->expects($this->once()) - ->method('enqueue') - ->with(new ToolListChangedNotification()); - - $registry = new Registry($notificationPublisher); - $registry->registerTool($tool, fn () => null); - - $this->assertSame($expected, $registry->getTools()); - } - - public function testResourceRegistration(): void - { - $resource = new Resource( - 'config://the-best-resource-uri-ever', - 'the-best-resource-name-ever', - ); - - $expected = [$resource->uri => $resource]; - - $notificationPublisher = $this->createMock(NotificationPublisher::class); - $notificationPublisher->expects($this->once()) - ->method('enqueue') - ->with(new ResourceListChangedNotification()); - - $registry = new Registry($notificationPublisher); - $registry->registerResource($resource, fn () => null); - - $this->assertSame($expected, $registry->getResources()); - } - - public function testPromptRegistration(): void - { - $prompt = new Prompt( - 'the-best-prompt-ever', - ); - - $expected = [$prompt->name => $prompt]; - - $notificationPublisher = $this->createMock(NotificationPublisher::class); - $notificationPublisher->expects($this->once()) - ->method('enqueue') - ->with(new PromptListChangedNotification()); - - $registry = new Registry($notificationPublisher); - $registry->registerPrompt($prompt, fn () => null); - - $this->assertSame($expected, $registry->getPrompts()); - } -} From dab3bf313d47c660711a9baf47e73a36dc05fcfb Mon Sep 17 00:00:00 2001 From: Bellangelo Date: Mon, 15 Sep 2025 23:57:52 +0300 Subject: [PATCH 28/28] Fix styling --- src/Capability/Registry.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Capability/Registry.php b/src/Capability/Registry.php index 5050216..44a1aa6 100644 --- a/src/Capability/Registry.php +++ b/src/Capability/Registry.php @@ -17,9 +17,6 @@ use Mcp\Capability\Registry\ResourceReference; use Mcp\Capability\Registry\ResourceTemplateReference; use Mcp\Capability\Registry\ToolReference; -use Mcp\Exception\InvalidArgumentException; -use Mcp\Schema\Content\PromptMessage; -use Mcp\Schema\Content\ResourceContents; use Mcp\Schema\Notification\PromptListChangedNotification; use Mcp\Schema\Notification\ResourceListChangedNotification; use Mcp\Schema\Notification\ToolListChangedNotification;