Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"autoload-dev": {
"psr-4": {
"Mcp\\Example\\Server\\CachedDiscovery\\": "examples/server/cached-discovery/",
"Mcp\\Example\\Server\\ChangeEvents\\": "examples/server/change-events/",
"Mcp\\Example\\Server\\ClientCommunication\\": "examples/server/client-communication/",
"Mcp\\Example\\Server\\ClientLogging\\": "examples/server/client-logging/",
"Mcp\\Example\\Server\\CombinedRegistration\\": "examples/server/combined-registration/",
Expand Down
66 changes: 66 additions & 0 deletions examples/server/change-events/ListChangingHandlers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/*
* 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\Example\Server\ChangeEvents;

use Mcp\Capability\RegistryInterface;
use Mcp\Schema\Content\PromptMessage;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\Role;
use Mcp\Schema\Prompt;
use Mcp\Schema\Resource;
use Mcp\Schema\Tool;

final class ListChangingHandlers
{
public function __construct(
private readonly RegistryInterface $registry,
) {
}

public function addPrompt(string $name, string $content): string
{
$this->registry->registerPrompt(
new Prompt($name),
static fn () => [new PromptMessage(Role::User, new TextContent($content))],
isManual: true,
);

return \sprintf('Prompt "%s" registered.', $name);
}

public function addResource(string $uri, string $name): string
{
$this->registry->registerResource(
new Resource($uri, $name),
static fn () => \sprintf('This is the content of the dynamically added resource "%s" at URI "%s".', $name, $uri),
true,
);

return \sprintf('Resource "%s" registered.', $name);
}

public function addTool(string $name): string
{
$this->registry->registerTool(
new Tool(
$name,
['type' => 'object', 'properties' => new \stdClass(), 'required' => []],
'Dynamically added tool',
null
),
static fn () => \sprintf('This is the output of the dynamically added tool "%s".', $name),
true,
);

return \sprintf('Tool "%s" registered.', $name);
}
}
46 changes: 46 additions & 0 deletions examples/server/change-events/server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/*
* 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.
*/

require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__);

use Mcp\Capability\Registry;
use Mcp\Capability\RegistryInterface;
use Mcp\Event\Dispatcher;
use Mcp\Event\ListenerProvider;
use Mcp\Example\Server\ChangeEvents\ListChangingHandlers;
use Mcp\Server;

logger()->info('Starting MCP Change Events Server...');

$listenerProvider = new ListenerProvider();
$dispatcher = new Dispatcher($listenerProvider);
$registry = new Registry($dispatcher, logger());
$container = container();
$container->set(RegistryInterface::class, $registry);

$server = Server::builder()
->setServerInfo('Server with Changing Lists', '1.0.0')
->setLogger(logger())
->setContainer($container)
->setRegistry($registry)
->setEventDispatcher($dispatcher)
->setEventListenerProvider($listenerProvider)
->addTool([ListChangingHandlers::class, 'addPrompt'], 'add_prompt', 'Tool that adds a new prompt to the registry with the given name and content.')
->addTool([ListChangingHandlers::class, 'addResource'], 'add_resource', 'Tool that adds a new resource to the registry with the given name and URL.')
->addTool([ListChangingHandlers::class, 'addTool'], 'add_tool', 'Tool that adds a new tool to the registry with the given name.')
->build();

$result = $server->run(transport());

logger()->info('Server listener stopped gracefully.', ['result' => $result]);

shutdown($result);
34 changes: 34 additions & 0 deletions src/Event/Dispatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* 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\Event;

use Psr\EventDispatcher\EventDispatcherInterface;

/**
* @author Christopher Hertel <[email protected]>
*/
final class Dispatcher implements EventDispatcherInterface
{
public function __construct(
private readonly ListenerProvider $listenerProvider,
) {
}

public function dispatch(object $event): object
{
foreach ($this->listenerProvider->getListenersForEvent($event) as $listener) {
$listener($event);
}

return $event;
}
}
44 changes: 44 additions & 0 deletions src/Event/ListenerProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/*
* 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\Event;

use Psr\EventDispatcher\ListenerProviderInterface;

/**
* Intended for SDK internal event listeners.
*
* @author Christopher Hertel <[email protected]>
*/
final class ListenerProvider implements ListenerProviderInterface
{
/**
* @var array <class-string, callable[]>
*/
private array $listeners = [];

public function addListener(string $eventClass, callable $listener): void
{
$this->listeners[$eventClass][] = $listener;
}

/**
* @return iterable<callable>
*/
public function getListenersForEvent(object $event): iterable
{
if (isset($this->listeners[$event::class])) {
foreach ($this->listeners[$event::class] as $listener) {
yield $listener;
}
}
}
}
4 changes: 2 additions & 2 deletions src/Schema/Tool.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
*
* @phpstan-type ToolInputSchema array{
* type: 'object',
* properties: array<string, mixed>,
* required: string[]|null
* properties: array<string, mixed>|object,
* required: string[]
* }
* @phpstan-type ToolOutputSchema array{
* type: 'object',
Expand Down
27 changes: 24 additions & 3 deletions src/Server/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
use Mcp\Capability\Registry\Loader\LoaderInterface;
use Mcp\Capability\Registry\ReferenceHandler;
use Mcp\Capability\RegistryInterface;
use Mcp\Event\Dispatcher;
use Mcp\Event\ListenerProvider;
use Mcp\Event\PromptListChangedEvent;
use Mcp\Event\ResourceListChangedEvent;
use Mcp\Event\ToolListChangedEvent;
use Mcp\JsonRpc\MessageFactory;
use Mcp\Schema\Annotations;
use Mcp\Schema\Enum\ProtocolVersion;
Expand Down Expand Up @@ -60,6 +65,8 @@ final class Builder

private ?EventDispatcherInterface $eventDispatcher = null;

private ?ListenerProvider $eventListenerProvider = null;

private ?ContainerInterface $container = null;

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

public function setEventListenerProvider(ListenerProvider $listenerProvider): self
{
$this->eventListenerProvider = $listenerProvider;

return $this;
}

/**
* Provides a PSR-11 DI container, primarily for resolving user-defined handler classes.
* Defaults to a basic internal container.
Expand Down Expand Up @@ -488,6 +502,8 @@ public function build(): Server
{
$logger = $this->logger ?? new NullLogger();
$container = $this->container ?? new Container();
$this->eventListenerProvider ??= new ListenerProvider();
$this->eventDispatcher ??= new Dispatcher($this->eventListenerProvider);
$registry = $this->registry ?? new Registry($this->eventDispatcher, $logger);

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

$capabilities = $this->serverCapabilities ?? new ServerCapabilities(
tools: $registry->hasTools(),
toolsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
toolsListChanged: true,
resources: $registry->hasResources() || $registry->hasResourceTemplates(),
resourcesSubscribe: false,
resourcesListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
resourcesListChanged: true,
prompts: $registry->hasPrompts(),
promptsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
promptsListChanged: true,
logging: true,
completions: true,
);
Expand Down Expand Up @@ -552,6 +568,11 @@ public function build(): Server
logger: $logger,
);

$changeListener = new ChangeListener($protocol);
$this->eventListenerProvider->addListener(PromptListChangedEvent::class, $changeListener->onPromptListChange(...));
$this->eventListenerProvider->addListener(ResourceListChangedEvent::class, $changeListener->onResourceListChange(...));
$this->eventListenerProvider->addListener(ToolListChangedEvent::class, $changeListener->onToolListChange(...));

return new Server($protocol, $logger);
}

Expand Down
42 changes: 42 additions & 0 deletions src/Server/ChangeListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* 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\Schema\Notification\PromptListChangedNotification;
use Mcp\Schema\Notification\ResourceListChangedNotification;
use Mcp\Schema\Notification\ToolListChangedNotification;

/**
* @author Christopher Hertel <[email protected]>
*/
final class ChangeListener
{
public function __construct(
private readonly Protocol $protocol,
) {
}

public function onPromptListChange(): void
{
$this->protocol->sendNotification(new PromptListChangedNotification());
}

public function onResourceListChange(): void
{
$this->protocol->sendNotification(new ResourceListChangedNotification());
}

public function onToolListChange(): void
{
$this->protocol->sendNotification(new ToolListChangedNotification());
}
}
Loading
Loading