Skip to content

Commit 704dc13

Browse files
committed
Wire up notifications for changing prompt, resource & tools lists
1 parent 940eb90 commit 704dc13

File tree

10 files changed

+318
-60
lines changed

10 files changed

+318
-60
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"autoload-dev": {
5555
"psr-4": {
5656
"Mcp\\Example\\Server\\CachedDiscovery\\": "examples/server/cached-discovery/",
57+
"Mcp\\Example\\Server\\ChangeEvents\\": "examples/server/change-events/",
5758
"Mcp\\Example\\Server\\ClientCommunication\\": "examples/server/client-communication/",
5859
"Mcp\\Example\\Server\\ClientLogging\\": "examples/server/client-logging/",
5960
"Mcp\\Example\\Server\\CombinedRegistration\\": "examples/server/combined-registration/",
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Example\Server\ChangeEvents;
13+
14+
use Mcp\Capability\RegistryInterface;
15+
use Mcp\Schema\Content\PromptMessage;
16+
use Mcp\Schema\Content\TextContent;
17+
use Mcp\Schema\Enum\Role;
18+
use Mcp\Schema\Prompt;
19+
use Mcp\Schema\Resource;
20+
use Mcp\Schema\Tool;
21+
22+
final class ListChangingHandlers
23+
{
24+
public function __construct(
25+
private readonly RegistryInterface $registry,
26+
) {
27+
}
28+
29+
public function addPrompt(string $name, string $content): string
30+
{
31+
$this->registry->registerPrompt(
32+
new Prompt($name),
33+
static fn () => [new PromptMessage(Role::User, new TextContent($content))],
34+
isManual: true,
35+
);
36+
37+
return \sprintf('Prompt "%s" registered.', $name);
38+
}
39+
40+
public function addResource(string $uri, string $name): string
41+
{
42+
$this->registry->registerResource(
43+
new Resource($uri, $name),
44+
static fn () => \sprintf('This is the content of the dynamically added resource "%s" at URI "%s".', $name, $uri),
45+
true,
46+
);
47+
48+
return \sprintf('Resource "%s" registered.', $name);
49+
}
50+
51+
public function addTool(string $name): string
52+
{
53+
$this->registry->registerTool(
54+
new Tool(
55+
$name,
56+
['type' => 'object', 'properties' => new \stdClass(), 'required' => []],
57+
'Dynamically added tool',
58+
null
59+
),
60+
static fn () => \sprintf('This is the output of the dynamically added tool "%s".', $name),
61+
true,
62+
);
63+
64+
return \sprintf('Tool "%s" registered.', $name);
65+
}
66+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
require_once dirname(__DIR__).'/bootstrap.php';
13+
chdir(__DIR__);
14+
15+
use Mcp\Capability\Registry;
16+
use Mcp\Capability\RegistryInterface;
17+
use Mcp\Event\Dispatcher;
18+
use Mcp\Event\ListenerProvider;
19+
use Mcp\Example\Server\ChangeEvents\ListChangingHandlers;
20+
use Mcp\Server;
21+
22+
logger()->info('Starting MCP Change Events Server...');
23+
24+
$listenerProvider = new ListenerProvider();
25+
$dispatcher = new Dispatcher($listenerProvider);
26+
$registry = new Registry($dispatcher, logger());
27+
$container = container();
28+
$container->set(RegistryInterface::class, $registry);
29+
30+
$server = Server::builder()
31+
->setServerInfo('Server with Changing Lists', '1.0.0')
32+
->setLogger(logger())
33+
->setContainer($container)
34+
->setRegistry($registry)
35+
->setEventDispatcher($dispatcher)
36+
->setEventListenerProvider($listenerProvider)
37+
->addTool([ListChangingHandlers::class, 'addPrompt'], 'add_prompt', 'Tool that adds a new prompt to the registry with the given name and content.')
38+
->addTool([ListChangingHandlers::class, 'addResource'], 'add_resource', 'Tool that adds a new resource to the registry with the given name and URL.')
39+
->addTool([ListChangingHandlers::class, 'addTool'], 'add_tool', 'Tool that adds a new tool to the registry with the given name.')
40+
->build();
41+
42+
$result = $server->run(transport());
43+
44+
logger()->info('Server listener stopped gracefully.', ['result' => $result]);
45+
46+
shutdown($result);

src/Event/Dispatcher.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Event;
13+
14+
use Psr\EventDispatcher\EventDispatcherInterface;
15+
16+
/**
17+
* @author Christopher Hertel <mail@christopher-hertel.de>
18+
*/
19+
final class Dispatcher implements EventDispatcherInterface
20+
{
21+
public function __construct(
22+
private readonly ListenerProvider $listenerProvider,
23+
) {
24+
}
25+
26+
public function dispatch(object $event): object
27+
{
28+
foreach ($this->listenerProvider->getListenersForEvent($event) as $listener) {
29+
$listener($event);
30+
}
31+
32+
return $event;
33+
}
34+
}

src/Event/ListenerProvider.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Event;
13+
14+
use Psr\EventDispatcher\ListenerProviderInterface;
15+
16+
/**
17+
* Intended for SDK internal event listeners.
18+
*
19+
* @author Christopher Hertel <mail@christopher-hertel.de>
20+
*/
21+
final class ListenerProvider implements ListenerProviderInterface
22+
{
23+
/**
24+
* @var array <class-string, callable[]>
25+
*/
26+
private array $listeners = [];
27+
28+
public function addListener(string $eventClass, callable $listener): void
29+
{
30+
$this->listeners[$eventClass][] = $listener;
31+
}
32+
33+
/**
34+
* @return iterable<callable>
35+
*/
36+
public function getListenersForEvent(object $event): iterable
37+
{
38+
if (isset($this->listeners[$event::class])) {
39+
foreach ($this->listeners[$event::class] as $listener) {
40+
yield $listener;
41+
}
42+
}
43+
}
44+
}

src/Schema/Tool.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
*
2222
* @phpstan-type ToolInputSchema array{
2323
* type: 'object',
24-
* properties: array<string, mixed>,
25-
* required: string[]|null
24+
* properties: array<string, mixed>|object,
25+
* required: string[]
2626
* }
2727
* @phpstan-type ToolOutputSchema array{
2828
* type: 'object',

src/Server/Builder.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
use Mcp\Capability\Registry\Loader\LoaderInterface;
2424
use Mcp\Capability\Registry\ReferenceHandler;
2525
use Mcp\Capability\RegistryInterface;
26+
use Mcp\Event\Dispatcher;
27+
use Mcp\Event\ListenerProvider;
28+
use Mcp\Event\PromptListChangedEvent;
29+
use Mcp\Event\ResourceListChangedEvent;
30+
use Mcp\Event\ToolListChangedEvent;
2631
use Mcp\JsonRpc\MessageFactory;
2732
use Mcp\Schema\Annotations;
2833
use Mcp\Schema\Enum\ProtocolVersion;
@@ -60,6 +65,8 @@ final class Builder
6065

6166
private ?EventDispatcherInterface $eventDispatcher = null;
6267

68+
private ?ListenerProvider $eventListenerProvider = null;
69+
6370
private ?ContainerInterface $container = null;
6471

6572
private ?SchemaGeneratorInterface $schemaGenerator = null;
@@ -284,6 +291,13 @@ public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): s
284291
return $this;
285292
}
286293

294+
public function setEventListenerProvider(ListenerProvider $listenerProvider): self
295+
{
296+
$this->eventListenerProvider = $listenerProvider;
297+
298+
return $this;
299+
}
300+
287301
/**
288302
* Provides a PSR-11 DI container, primarily for resolving user-defined handler classes.
289303
* Defaults to a basic internal container.
@@ -488,6 +502,8 @@ public function build(): Server
488502
{
489503
$logger = $this->logger ?? new NullLogger();
490504
$container = $this->container ?? new Container();
505+
$this->eventListenerProvider ??= new ListenerProvider();
506+
$this->eventDispatcher ??= new Dispatcher($this->eventListenerProvider);
491507
$registry = $this->registry ?? new Registry($this->eventDispatcher, $logger);
492508

493509
$loaders = [
@@ -511,12 +527,12 @@ public function build(): Server
511527

512528
$capabilities = $this->serverCapabilities ?? new ServerCapabilities(
513529
tools: $registry->hasTools(),
514-
toolsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
530+
toolsListChanged: true,
515531
resources: $registry->hasResources() || $registry->hasResourceTemplates(),
516532
resourcesSubscribe: false,
517-
resourcesListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
533+
resourcesListChanged: true,
518534
prompts: $registry->hasPrompts(),
519-
promptsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
535+
promptsListChanged: true,
520536
logging: true,
521537
completions: true,
522538
);
@@ -552,6 +568,11 @@ public function build(): Server
552568
logger: $logger,
553569
);
554570

571+
$changeListener = new ChangeListener($protocol);
572+
$this->eventListenerProvider->addListener(PromptListChangedEvent::class, $changeListener->onPromptListChange(...));
573+
$this->eventListenerProvider->addListener(ResourceListChangedEvent::class, $changeListener->onResourceListChange(...));
574+
$this->eventListenerProvider->addListener(ToolListChangedEvent::class, $changeListener->onToolListChange(...));
575+
555576
return new Server($protocol, $logger);
556577
}
557578

src/Server/ChangeListener.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Server;
13+
14+
use Mcp\Schema\Notification\PromptListChangedNotification;
15+
use Mcp\Schema\Notification\ResourceListChangedNotification;
16+
use Mcp\Schema\Notification\ToolListChangedNotification;
17+
18+
/**
19+
* @author Christopher Hertel <mail@christopher-hertel.de>
20+
*/
21+
final class ChangeListener
22+
{
23+
public function __construct(
24+
private readonly Protocol $protocol,
25+
) {
26+
}
27+
28+
public function onPromptListChange(): void
29+
{
30+
$this->protocol->sendNotification(new PromptListChangedNotification());
31+
}
32+
33+
public function onResourceListChange(): void
34+
{
35+
$this->protocol->sendNotification(new ResourceListChangedNotification());
36+
}
37+
38+
public function onToolListChange(): void
39+
{
40+
$this->protocol->sendNotification(new ToolListChangedNotification());
41+
}
42+
}

0 commit comments

Comments
 (0)